diff --git a/bin/package-electron b/bin/package-electron index 8203cc99df6..164382f454f 100755 --- a/bin/package-electron +++ b/bin/package-electron @@ -34,10 +34,17 @@ if [ "$OSTYPE" == "msys" ]; then fi fi -yarn workspace loot-core build:node +# TODO: should do these async if possible +# required for when running in electron +yarn workspace @actual-app/crdt build +yarn workspace loot-core build:node yarn workspace @actual-app/web build --mode=desktop # electron specific build +# required for when running in web server (exposed via ngrok) +yarn workspace loot-core build:browser +yarn workspace @actual-app/web build:browser + yarn workspace desktop-electron update-client ( diff --git a/package.json b/package.json index a76e1e29034..88daa9fd3fd 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "start": "yarn start:browser", "start:desktop": "yarn rebuild-electron && npm-run-all --parallel 'start:desktop-*'", "start:desktop-node": "yarn workspace loot-core watch:node", + "start:desktop-server-dependencies": "yarn workspace @actual-app/crdt build", "start:desktop-client": "yarn workspace @actual-app/web watch", "start:desktop-electron": "yarn workspace desktop-electron watch", "start:electron": "yarn start:desktop", diff --git a/packages/desktop-client/src/browser-preload.browser.js b/packages/desktop-client/src/browser-preload.browser.js index 69834cca51c..7dc89d2f82f 100644 --- a/packages/desktop-client/src/browser-preload.browser.js +++ b/packages/desktop-client/src/browser-preload.browser.js @@ -156,6 +156,9 @@ global.Actual = { openURLInBrowser: url => { window.open(url, '_blank'); }, + downloadActualServer: () => {}, + startActualServer: () => {}, + exposeActualServer: () => {}, onEventFromMain: () => {}, isUpdateReadyForDownload: () => isUpdateReadyForDownload, waitForUpdateReadyForDownload: () => isUpdateReadyForDownloadPromise, diff --git a/packages/desktop-client/src/components/manager/ConfigExternalSyncServer.tsx b/packages/desktop-client/src/components/manager/ConfigExternalSyncServer.tsx new file mode 100644 index 00000000000..bcacaca582d --- /dev/null +++ b/packages/desktop-client/src/components/manager/ConfigExternalSyncServer.tsx @@ -0,0 +1,237 @@ +// @ts-strict-ignore +import React, { useState, useEffect, useCallback } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { type To } from 'react-router-dom'; + +import { + isNonProductionEnvironment, + isElectron, +} from 'loot-core/src/shared/environment'; + +import { useActions } from '../../hooks/useActions'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useNavigate } from '../../hooks/useNavigate'; +import { theme } from '../../style'; +import { Button, ButtonWithLoading } from '../common/Button2'; +import { BigInput } from '../common/Input'; +import { Link } from '../common/Link'; +import { Text } from '../common/Text'; +import { View } from '../common/View'; +import { useServerURL, useSetServerURL } from '../ServerContext'; + +import { Title } from './subscribe/common'; + +export function ConfigExternalSyncServer() { + const { t } = useTranslation(); + const { createBudget, signOut, loggedIn } = useActions(); + const navigate = useNavigate(); + const [url, setUrl] = useState(''); + const currentUrl = useServerURL(); + const setServerUrl = useSetServerURL(); + useEffect(() => { + setUrl(currentUrl); + }, [currentUrl]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const restartElectronServer = useCallback(() => { + globalThis.window.Actual.restartElectronServer(); + setError(null); + }, []); + + const [_serverSelfSignedCert, setServerSelfSignedCert] = useGlobalPref( + 'serverSelfSignedCert', + restartElectronServer, + ); + + function getErrorMessage(error: string) { + switch (error) { + case 'network-failure': + return t( + 'Server is not running at this URL. Make sure you have HTTPS set up properly.', + ); + default: + return t( + 'Server does not look like an Actual server. Is it set up correctly?', + ); + } + } + + async function onSubmit() { + if (url === '' || loading) { + return; + } + + setError(null); + setLoading(true); + const { error } = await setServerUrl(url); + + if ( + ['network-failure', 'get-server-failure'].includes(error) && + !url.startsWith('http://') && + !url.startsWith('https://') + ) { + const { error } = await setServerUrl('https://' + url); + if (error) { + setUrl('https://' + url); + setError(error); + } else { + await signOut(); + navigate('/'); + } + setLoading(false); + } else if (error) { + setLoading(false); + setError(error); + } else { + setLoading(false); + await signOut(); + navigate('/'); + } + } + + function onSameDomain() { + setUrl(window.location.origin); + } + + async function onSelectSelfSignedCertificate() { + const selfSignedCertificateLocation = await window.Actual?.openFileDialog({ + properties: ['openFile'], + filters: [ + { + name: 'Self Signed Certificate', + extensions: ['crt', 'pem'], + }, + ], + }); + + if (selfSignedCertificateLocation) { + setServerSelfSignedCert(selfSignedCertificateLocation[0]); + } + } + + function onStopUsingExternalServer() { + setServerUrl(null); + loggedIn(); + navigate('/'); + } + + function onBack() { + // If server url is setup, go back to files manager, otherwise go to server setup + if (currentUrl) { + navigate('/'); + } else { + navigate(-1); + } + } + + return ( + + + + <Text + style={{ + fontSize: 16, + color: theme.tableRowHeaderText, + lineHeight: 1.5, + }} + > + {currentUrl ? ( + <Trans> + Existing sessions will be logged out and you will log in to this + server. We will validate that Actual is running at this URL. + </Trans> + ) : ( + <Trans> + After running the server, specify the URL here to use it. We will + validate that Actual is running at this URL. + </Trans> + )} + </Text> + + {error && ( + <> + <Text + style={{ + marginTop: 20, + color: theme.errorText, + borderRadius: 4, + fontSize: 15, + }} + > + {getErrorMessage(error)} + </Text> + {isElectron() && ( + <View + style={{ display: 'flex', flexDirection: 'row', marginTop: 20 }} + > + <Text + style={{ + color: theme.errorText, + borderRadius: 4, + fontSize: 15, + }} + > + <Trans> + If the server is using a self-signed certificate{' '} + <Link + variant="text" + style={{ fontSize: 15 }} + onClick={onSelectSelfSignedCertificate} + > + select it here + </Link> + . + </Trans> + </Text> + </View> + )} + </> + )} + + <View style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}> + <BigInput + autoFocus={true} + placeholder={t('https://example.com')} + value={url || ''} + onChangeValue={setUrl} + style={{ flex: 1, marginRight: 10 }} + onEnter={onSubmit} + /> + <ButtonWithLoading + variant="primary" + isLoading={loading} + style={{ fontSize: 15 }} + onPress={onSubmit} + > + {t('OK')} + </ButtonWithLoading> + {currentUrl && ( + <Button + variant="bare" + style={{ fontSize: 15, marginLeft: 10 }} + onPress={() => navigate(-1)} + > + {t('Cancel')} + </Button> + )} + </View> + <View + style={{ + flexDirection: 'row', + flexFlow: 'row wrap', + justifyContent: 'center', + marginTop: 15, + gap: '10px', + }} + > + <Button onPress={onBack}>Back</Button> + {currentUrl && ( + <Button onPress={onStopUsingExternalServer}> + Stop using external server + </Button> + )} + </View> + </View> + ); +} diff --git a/packages/desktop-client/src/components/manager/ConfigInternalSyncServer.tsx b/packages/desktop-client/src/components/manager/ConfigInternalSyncServer.tsx new file mode 100644 index 00000000000..805899d79dc --- /dev/null +++ b/packages/desktop-client/src/components/manager/ConfigInternalSyncServer.tsx @@ -0,0 +1,180 @@ +// @ts-strict-ignore +import React, { useState, useEffect, useCallback } from 'react'; +import { Group, NumberField } from 'react-aria-components'; +import { Trans, useTranslation } from 'react-i18next'; + +import { loggedIn } from 'loot-core/client/actions'; + +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useNavigate } from '../../hooks/useNavigate'; +import { styles, theme } from '../../style'; +import { Button, ButtonWithLoading } from '../common/Button2'; +import { Input } from '../common/Input'; +import { Label } from '../common/Label'; +import { Text } from '../common/Text'; +import { View } from '../common/View'; +import { useResponsive } from '../responsive/ResponsiveProvider'; + +import { Title } from './subscribe/common'; + +export function ConfigInternalSyncServer() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [serverConfig, setServerConfig] = useState({ + port: 5007, + certificatePath: undefined, // merge with current global.json pref + ngrokDomain: undefined, + ngrokAuthToken: undefined, + }); + + const startActualServer = async () => { + await globalThis.Actual.startActualServer('v24.10.1'); + }; + + const { isNarrowWidth } = useResponsive(); + const narrowButtonStyle = isNarrowWidth + ? { + height: styles.mobileMinHeight, + } + : {}; + + const [ngrokConfig, setNgrokConfig] = useGlobalPref('ngrokConfig'); + + const exposeActualServer = async () => { + const hasRequiredNgrokSettings = + ngrokConfig?.authToken && ngrokConfig?.port && ngrokConfig?.domain; + if (hasRequiredNgrokSettings) { + const url = await globalThis.Actual.exposeActualServer({ + authToken: ngrokConfig.authToken, + port: ngrokConfig.port, + domain: ngrokConfig.domain, + }); + + console.info('exposing actual at: ' + url); + } else { + console.info('ngrok settings not set'); + } + }; + + const handleChange = (name: keyof typeof serverConfig, event) => { + console.info(event); + const { value } = event.target; + setServerConfig({ + ...serverConfig, + [name]: value, + }); + }; + + function onStopUsingInternalServer() { + setNgrokConfig(undefined); + loggedIn(); + navigate('/'); + } + + function onBack() { + // If ngrok is setup, go back to files manager, otherwise go to server setup + if (ngrokConfig) { + navigate('/'); + } else { + navigate(-1); + } + } + + return ( + <View style={{ maxWidth: 500, marginTop: -30 }}> + <Title text={t('Server configuration')} /> + + <View> + <Text + style={{ + fontSize: 16, + color: theme.pageText, + lineHeight: 1.5, + }} + > + <Trans> + Actual can setup a server for you to sync your data across devices. + It can either run on your computer or on a server. If you want to + run it on your computer, you can use the button below to start the + server. If you want to run it on a server, you can enter the URL of + the server below. + </Trans> + </Text> + + <View> + <Label title={t('Port')} /> + <Input + type="number" + value={serverConfig.port} + name={t('Port')} + onChange={event => handleChange('port', event)} + /> + </View> + + <View> + <Label title={t('SSL Certificate (optional)')} /> + <Input + type="file" + value={serverConfig.certificatePath} + name={t('Certificate')} + onChange={event => handleChange('certificatePath', event)} + /> + </View> + + <View> + <Label title={t('Ngrok custom domain (optional)')} /> + <Input + type="text" + value={serverConfig.ngrokDomain} + name={t('Ngrok Custom Domain')} + onChange={event => handleChange('ngrokDomain', event)} + /> + </View> + <View> + <Label title={t('Ngrok auth token (optional)')} /> + <Input + type="text" + value={serverConfig.ngrokAuthToken} + name={t('Ngrok Auth Token')} + onChange={event => handleChange('ngrokAuthToken', event)} + /> + </View> + </View> + <View + style={{ + flexDirection: 'row', + flexFlow: 'row wrap', + justifyContent: 'center', + marginTop: 15, + gap: '10px', + }} + > + <Button onPress={onBack}>Back</Button> + {ngrokConfig && ( + <Button onPress={onStopUsingInternalServer}> + Stop using internal server + </Button> + )} + + <Button + variant="primary" + onPress={startActualServer} + style={{ + ...narrowButtonStyle, + }} + > + Start Server + </Button> + <Button + variant="primary" + onPress={exposeActualServer} + style={{ + ...narrowButtonStyle, + }} + > + Expose Server + </Button> + </View> + </View> + ); +} diff --git a/packages/desktop-client/src/components/manager/ConfigServer-failedattempt.tsx b/packages/desktop-client/src/components/manager/ConfigServer-failedattempt.tsx new file mode 100644 index 00000000000..0e48e50ec23 --- /dev/null +++ b/packages/desktop-client/src/components/manager/ConfigServer-failedattempt.tsx @@ -0,0 +1,70 @@ +// @ts-strict-ignore +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { isElectron } from 'loot-core/src/shared/environment'; + +import { useActions } from '../../hooks/useActions'; +import { useNavigate } from '../../hooks/useNavigate'; +import { theme } from '../../style'; +import { Button } from '../common/Button2'; +import { Text } from '../common/Text'; +import { View } from '../common/View'; +import { useSetServerURL } from '../ServerContext'; + +import { Title } from './subscribe/common'; + +export function ConfigServerDelMe() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const setServerUrl = useSetServerURL(); + const { loggedIn } = useActions(); + + async function onSkip() { + await setServerUrl(null); + await loggedIn(); + navigate('/'); + } + + return ( + <View style={{ maxWidth: 500, marginTop: -30 }}> + <Title text={t('Let’s set up your server!')} /> + + <Text + style={{ + fontSize: 16, + color: theme.pageText, + lineHeight: 1.5, + }} + > + <Trans> + If you like, Actual can setup a server for you to sync your data + across devices. + </Trans> + </Text> + {isElectron() && ( + <> + <Text + style={{ + fontSize: 16, + color: theme.pageText, + lineHeight: 1.5, + }} + > + <Trans> + Would you like to host the server on your computer or connect to + an external server? + </Trans> + </Text> + <Button onPress={() => navigate('/config-server/internal')}> + Host on this computer + </Button> + </> + )} + <Button onPress={() => navigate('/config-server/external')}> + Connect to an external server + </Button> + <Button onPress={onSkip}>I don’t want a server</Button> + </View> + ); +} diff --git a/packages/desktop-client/src/components/manager/ConfigServer.tsx b/packages/desktop-client/src/components/manager/ConfigServer.tsx index a8020f083dc..5723f28fe39 100644 --- a/packages/desktop-client/src/components/manager/ConfigServer.tsx +++ b/packages/desktop-client/src/components/manager/ConfigServer.tsx @@ -1,8 +1,8 @@ // @ts-strict-ignore -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useEffect } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { createBudget, loggedIn, signOut } from 'loot-core/client/actions'; +import { createBudget, loggedIn } from 'loot-core/client/actions'; import { isNonProductionEnvironment, isElectron, @@ -12,9 +12,7 @@ import { useGlobalPref } from '../../hooks/useGlobalPref'; import { useNavigate } from '../../hooks/useNavigate'; import { useDispatch } from '../../redux'; import { theme } from '../../style'; -import { Button, ButtonWithLoading } from '../common/Button2'; -import { BigInput } from '../common/Input'; -import { Link } from '../common/Link'; +import { Button } from '../common/Button2'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { useServerURL, useSetServerURL } from '../ServerContext'; @@ -25,83 +23,16 @@ export function ConfigServer() { const { t } = useTranslation(); const dispatch = useDispatch(); const navigate = useNavigate(); - const [url, setUrl] = useState(''); const currentUrl = useServerURL(); + const [ngrokConfig] = useGlobalPref('ngrokConfig'); const setServerUrl = useSetServerURL(); - useEffect(() => { - setUrl(currentUrl); - }, [currentUrl]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState<string | null>(null); - - const restartElectronServer = useCallback(() => { - globalThis.window.Actual.restartElectronServer(); - setError(null); - }, []); - - const [_serverSelfSignedCert, setServerSelfSignedCert] = useGlobalPref( - 'serverSelfSignedCert', - restartElectronServer, - ); - - function getErrorMessage(error: string) { - switch (error) { - case 'network-failure': - return t( - 'Server is not running at this URL. Make sure you have HTTPS set up properly.', - ); - default: - return t( - 'Server does not look like an Actual server. Is it set up correctly?', - ); - } - } - - async function onSubmit() { - if (url === '' || loading) { - return; - } - - setError(null); - setLoading(true); - - let httpUrl = url; - if (!url.startsWith('http://') && !url.startsWith('https://')) { - httpUrl = 'https://' + url; - } - - const { error } = await setServerUrl(httpUrl); - setUrl(httpUrl); - - if (error) { - setLoading(false); - setError(error); - } else { - setLoading(false); - await dispatch(signOut()); - navigate('/'); - } - } - - function onSameDomain() { - setUrl(window.location.origin); - } - - async function onSelectSelfSignedCertificate() { - const selfSignedCertificateLocation = await window.Actual?.openFileDialog({ - properties: ['openFile'], - filters: [ - { - name: 'Self Signed Certificate', - extensions: ['crt', 'pem'], - }, - ], - }); + const onShowExternalConfiguration = () => { + navigate('/config-server/external'); + }; - if (selfSignedCertificateLocation) { - setServerSelfSignedCert(selfSignedCertificateLocation[0]); - } - } + const onShowInternalConfiguration = () => { + navigate('/config-server/internal'); + }; async function onSkip() { await setServerUrl(null); @@ -115,98 +46,80 @@ export function ConfigServer() { navigate('/'); } + // let serverConfiguration = undefined; + // if (currentUrl) { + // serverConfiguration = 'external'; + // } else if (ngrokConfig) { + // serverConfiguration = 'internal'; + // } + useEffect(() => { + // If user has already setup server navigate them to the configure screen + // TODO: make an easy setting to determin if internal or external server is setup + if (currentUrl) { + // external + navigate('/config-server/external'); + } else if (ngrokConfig) { + // internal + navigate('/config-server/internal'); + } + }, [currentUrl, navigate, ngrokConfig]); + return ( <View style={{ maxWidth: 500, marginTop: -30 }}> - <Title text={t('Where’s the server?')} /> - - <Text - style={{ - fontSize: 16, - color: theme.tableRowHeaderText, - lineHeight: 1.5, - }} - > - {currentUrl ? ( - <Trans> - Existing sessions will be logged out and you will log in to this - server. We will validate that Actual is running at this URL. - </Trans> - ) : ( - <Trans> - There is no server configured. After running the server, specify the - URL here to use the app. You can always change this later. We will - validate that Actual is running at this URL. - </Trans> - )} - </Text> - - {error && ( - <> + <Title text={t('Setup your server')} /> + + {isElectron() && ( + <View + style={{ + flexDirection: 'column', + gap: '1rem', + alignItems: 'center', + marginBottom: '20px', + }} + > <Text style={{ - marginTop: 20, - color: theme.errorText, - borderRadius: 4, - fontSize: 15, + fontSize: 16, + color: theme.pageText, + lineHeight: 1.5, }} > - {getErrorMessage(error)} + <Trans> + If you like, Actual can connect to a server for you to sync your + data across devices. + </Trans> </Text> - {isElectron() && ( - <View - style={{ display: 'flex', flexDirection: 'row', marginTop: 20 }} - > - <Text - style={{ - color: theme.errorText, - borderRadius: 4, - fontSize: 15, - }} - > - <Trans> - If the server is using a self-signed certificate{' '} - <Link - variant="text" - style={{ fontSize: 15 }} - onClick={onSelectSelfSignedCertificate} - > - select it here - </Link> - . - </Trans> - </Text> - </View> - )} - </> - )} - - <View style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}> - <BigInput - autoFocus={true} - placeholder={t('https://example.com')} - value={url || ''} - onChangeValue={setUrl} - style={{ flex: 1, marginRight: 10 }} - onEnter={onSubmit} - /> - <ButtonWithLoading - variant="primary" - isLoading={loading} - style={{ fontSize: 15 }} - onPress={onSubmit} - > - {t('OK')} - </ButtonWithLoading> - {currentUrl && ( - <Button - variant="bare" - style={{ fontSize: 15, marginLeft: 10 }} - onPress={() => navigate(-1)} + <Text + style={{ + fontSize: 16, + color: theme.pageText, + lineHeight: 1.5, + }} > - {t('Cancel')} - </Button> - )} - </View> + <Trans> + Would you like to host the server on your computer or connect to + an external server? + </Trans> + </Text> + <View + style={{ + flexDirection: 'row', + flexFlow: 'row wrap', + justifyContent: 'center', + marginTop: 15, + gap: '15px', + }} + > + <Button onPress={onShowInternalConfiguration}> + Host on this computer + </Button> + + <Button onPress={onShowExternalConfiguration}> + Connect to an external server + </Button> + </View> + </View> + )} <View style={{ @@ -226,19 +139,6 @@ export function ConfigServer() { </Button> ) : ( <> - {!isElectron() && ( - <Button - variant="bare" - style={{ - color: theme.pageTextLight, - margin: 5, - marginRight: 15, - }} - onPress={onSameDomain} - > - {t('Use current domain')} - </Button> - )} <Button variant="bare" style={{ color: theme.pageTextLight, margin: 5 }} diff --git a/packages/desktop-client/src/components/manager/ManagementApp.tsx b/packages/desktop-client/src/components/manager/ManagementApp.tsx index 872bf98363a..f1d584b731d 100644 --- a/packages/desktop-client/src/components/manager/ManagementApp.tsx +++ b/packages/desktop-client/src/components/manager/ManagementApp.tsx @@ -22,7 +22,9 @@ import { useResponsive } from '../responsive/ResponsiveProvider'; import { useMultiuserEnabled, useServerVersion } from '../ServerContext'; import { BudgetList } from './BudgetList'; -import { ConfigServer } from './ConfigServer'; +import { ConfigExternalSyncServer } from './ConfigExternalSyncServer'; +import { ConfigInternalSyncServer } from './ConfigInternalSyncServer'; +import { ConfigServer, loader as ConfigServerLoader } from './ConfigServer'; import { ServerURL } from './ServerURL'; import { Bootstrap } from './subscribe/Bootstrap'; import { ChangePassword } from './subscribe/ChangePassword'; @@ -129,6 +131,14 @@ export function ManagementApp() { <> <Routes> <Route path="/config-server" element={<ConfigServer />} /> + <Route + path="/config-server/external" + element={<ConfigExternalSyncServer />} + /> + <Route + path="/config-server/internal" + element={<ConfigInternalSyncServer />} + /> <Route path="/change-password" element={<ChangePassword />} /> {files && files.length > 0 ? ( diff --git a/packages/desktop-electron/index.ts b/packages/desktop-electron/index.ts index 3a910e05573..c675d6f1e69 100644 --- a/packages/desktop-electron/index.ts +++ b/packages/desktop-electron/index.ts @@ -2,6 +2,7 @@ import fs from 'fs'; import { createServer, Server } from 'http'; import path from 'path'; +import ngrok from '@ngrok/ngrok'; import { net, app, @@ -19,9 +20,11 @@ import { Env, ForkOptions, } from 'electron'; -import { copy, exists, remove } from 'fs-extra'; +import { copy, exists, mkdir, remove } from 'fs-extra'; import promiseRetry from 'promise-retry'; +import { GlobalPrefs } from 'loot-core/types/prefs'; + import { getMenu } from './menu'; import { get as getWindowState, @@ -54,6 +57,7 @@ if (!isDev || !process.env.ACTUAL_DATA_DIR) { // be closed automatically when the JavaScript object is garbage collected. let clientWin: BrowserWindow | null; let serverProcess: UtilityProcess | null; +let actualServerProcess: UtilityProcess | null; let oAuthServer: ReturnType<typeof createServer> | null; @@ -175,6 +179,164 @@ async function createBackgroundProcess() { }); } +function startSyncServer() { + const syncServerConfig = { + port: 5007, + ACTUAL_SERVER_DATA_DIR: path.resolve( + process.env.ACTUAL_DATA_DIR, + 'actual-server', + ), + ACTUAL_SERVER_FILES: path.resolve( + process.env.ACTUAL_DATA_DIR, + 'actual-server', + 'server-files', + ), + ACTUAL_USER_FILES: path.resolve( + process.env.ACTUAL_DATA_DIR, + 'actual-server', + 'user-files', + ), + defaultDataDir: path.resolve( + // TODO: There's no env variable for this - may need to add one to sync server + process.env.ACTUAL_DATA_DIR, + 'actual-server', + 'data', + ), + https: { + key: '', + cert: '', + }, + }; + + const serverPath = path.resolve( + __dirname, + isDev + ? '../../../node_modules/actual-sync/app.js' + : '../node_modules/actual-sync/app.js', // Temporary - required because actual-server is in the other repo + ); + + // NOTE: config.json parameters will be relative to THIS directory at the moment - may need a fix? + // Or we can override the config.json location when starting the process + let envVariables: Env = { + ...process.env, // required + ACTUAL_PORT: `${syncServerConfig.port}`, + ACTUAL_SERVER_FILES: `${syncServerConfig.ACTUAL_SERVER_FILES}`, + ACTUAL_USER_FILES: `${syncServerConfig.ACTUAL_USER_FILES}`, + ACTUAL_DATA_DIR: `${syncServerConfig.ACTUAL_SERVER_DATA_DIR}`, + }; + + const webRoot = path.resolve( + __dirname, + isDev + ? '../../../node_modules/@actual-app/web/build/' // workspace node_modules + : '../node_modules/@actual-app/web/build/', // location of packaged module + ); + + envVariables = { ...envVariables, ACTUAL_WEB_ROOT: webRoot }; + + // const customDomain = globalPrefs?.ngrokConfig?.domain; + + // if (customDomain) { + // // If we expose on a custom domain via ngrok we need to tell the server to allow it to work as a proxy + // // I'm not sure about this. It needs a CIDR block. I'm not sure what to put here or if it is needed. + // // It's possible this setting will prevent the annoying auth issue where I have to login every day + // envVariables = { ...envVariables, ACTUAL_TRUSTED_PROXIES: customDomain }; + // } + + if (!fs.existsSync(syncServerConfig.ACTUAL_SERVER_FILES)) { + // create directories if they do not exit - actual-sync doesn't do it for us... + mkdir(syncServerConfig.ACTUAL_SERVER_FILES, { recursive: true }); + } + + if (!fs.existsSync(syncServerConfig.ACTUAL_USER_FILES)) { + // create directories if they do not exit - actual-sync doesn't do it for us... + mkdir(syncServerConfig.ACTUAL_USER_FILES, { recursive: true }); + } + + // TODO: make sure .migrate file is also in user-directory under actual-server + + let forkOptions: ForkOptions = { + stdio: 'pipe', + env: envVariables, + }; + + if (isDev) { + forkOptions = { ...forkOptions, execArgv: ['--inspect'] }; + } + + const SYNC_SERVER_WAIT_TIMEOUT = 10000; // wait 10 seconds for the server to start - if it doesn't, throw an error + + let syncServerStarted = false; + + const syncServerPromise = new Promise<void>(async resolve => { + actualServerProcess = utilityProcess.fork( + serverPath, // This requires actual-server depencies (crdt) to be built before running electron - they need to be manually specified because actual-server doesn't get bundled + [], + forkOptions, + ); + + actualServerProcess.stdout?.on('data', (chunk: Buffer) => { + // Send the Server console.log messages to the main browser window + const chunkValue = JSON.stringify(chunk.toString('utf8')); + if (chunkValue.includes('Listening on')) { + // can we send a signal from the server instead of doing this? + console.info('Actual Sync Server has started!'); + syncServerStarted = true; + resolve(); + } + + clientWin?.webContents.executeJavaScript(` + console.info('Actual Sync Server Log:', ${chunkValue})`); + }); + + actualServerProcess.stderr?.on('data', (chunk: Buffer) => { + // Send the Server console.error messages out to the main browser window + clientWin?.webContents.executeJavaScript(` + console.error('Actual Sync Server Log:', ${JSON.stringify(chunk.toString('utf8'))})`); + }); + }); + + const syncServerTimeout = new Promise<void>((_, reject) => { + setTimeout(() => { + if (!syncServerStarted) { + const errorMessage = `Sync server failed to start within ${SYNC_SERVER_WAIT_TIMEOUT / 1000} seconds. Something is wrong. Please raise a github issue.`; + console.error(errorMessage); + reject(new Error(errorMessage)); + } + }, SYNC_SERVER_WAIT_TIMEOUT); + }); + + // This aint working... + return Promise.race([syncServerPromise, syncServerTimeout]); // Either the server has started or the timeout is reached +} + +async function exposeSyncServer(ngrokConfig: GlobalPrefs['ngrokConfig']) { + const hasRequiredConfig = + ngrokConfig?.authToken && ngrokConfig?.domain && ngrokConfig?.port; + + if (!hasRequiredConfig) { + console.error('Cannot expose sync server: missing ngrok settings'); + return { error: 'Missing ngrok settings' }; + } + + try { + const listener = await ngrok.forward({ + schemes: ['https'], // change this to https and bind certificate - may need to generate cert and store in user-data + addr: ngrokConfig.port, + authtoken: ngrokConfig.authToken, + domain: ngrokConfig.domain, + // crt: fs.readFileSync("crt.pem", "utf8"), + // key: fs.readFileSync("key.pem", "utf8"), + }); + + console.info(`Exposing actual server on url: ${listener.url()}`); + return { url: listener.url() }; + } catch (error) { + console.error('Unable to run ngrok', error); + return { error: `Unable to run ngrok. ${error}` }; + } +} + async function createWindow() { const windowState = await getWindowState(); @@ -305,6 +467,21 @@ app.on('ready', async () => { // Install an `app://` protocol that always returns the base HTML // file no matter what URL it is. This allows us to use react-router // on the frontend + + const globalPrefs = await loadGlobalPrefs(); // load global prefs + + const ngrokConfig = globalPrefs.ngrokConfig as + | { autoStart: boolean } + | undefined; // euuughhh + + if (ngrokConfig?.autoStart) { + // wait for both server and ngrok to start before starting the Actual client to ensure server is available + await Promise.allSettled([ + startSyncServer(), + exposeSyncServer(globalPrefs.ngrokConfig), + ]); + } + protocol.handle('app', request => { if (request.method !== 'GET') { return new Response(null, { @@ -360,7 +537,7 @@ app.on('ready', async () => { console.log('Suspending', new Date()); }); - createBackgroundProcess(); + await createBackgroundProcess(); }); app.on('window-all-closed', () => { @@ -461,6 +638,14 @@ ipcMain.handle('open-external-url', (event, url) => { shell.openExternal(url); }); +ipcMain.handle('start-actual-server', async () => startSyncServer()); + +ipcMain.handle( + 'expose-actual-server', + async (_event, payload: GlobalPrefs['ngrokConfig']) => + exposeSyncServer(payload), +); + ipcMain.on('message', (_event, msg) => { if (!serverProcess) { return; diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 0f48508defb..f9f757305bf 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -56,20 +56,10 @@ }, "win": { "target": [ - { - "target": "appx", - "arch": [ - "ia32", - "x64", - "arm64" - ] - }, { "target": "nsis", "arch": [ - "ia32", - "x64", - "arm64" + "x64" ] } ], @@ -85,6 +75,8 @@ "npmRebuild": false }, "dependencies": { + "@ngrok/ngrok": "^1.4.1", + "actual-sync": "*", "better-sqlite3": "^11.7.0", "fs-extra": "^11.2.0", "promise-retry": "^2.0.1" diff --git a/packages/desktop-electron/preload.ts b/packages/desktop-electron/preload.ts index 85b282dcb12..c57a78776fd 100644 --- a/packages/desktop-electron/preload.ts +++ b/packages/desktop-electron/preload.ts @@ -1,5 +1,7 @@ import { ipcRenderer, contextBridge, IpcRenderer } from 'electron'; +import { GlobalPrefs } from 'loot-core/types/prefs'; + import { GetBootstrapDataPayload, OpenFileDialogPayload, @@ -59,6 +61,16 @@ contextBridge.exposeInMainWorld('Actual', { ipcRenderer.invoke('open-external-url', url); }, + startActualServer: (releaseVersion: string) => { + return ipcRenderer.invoke('start-actual-server', { + releaseVersion, + }); + }, + + exposeActualServer: (settings: GlobalPrefs['ngrokConfig']) => { + return ipcRenderer.invoke('expose-actual-server', settings); + }, + onEventFromMain: (type: string, handler: (...args: unknown[]) => void) => { ipcRenderer.on(type, handler); }, diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.d.ts b/packages/loot-core/src/platform/server/asyncStorage/index.d.ts index 4a189f3a86e..6cd9ac22aa2 100644 --- a/packages/loot-core/src/platform/server/asyncStorage/index.d.ts +++ b/packages/loot-core/src/platform/server/asyncStorage/index.d.ts @@ -1,7 +1,7 @@ export function init(opts?: { persist?: boolean }): void; export type Init = typeof init; -export function getItem(key: string): Promise<string>; +export function getItem<T = string>(key: string): Promise<T>; export type GetItem = typeof getItem; export function setItem(key: string, value: unknown): void; @@ -10,7 +10,7 @@ export type SetItem = typeof setItem; export function removeItem(key: string): void; export type RemoveItem = typeof removeItem; -export function multiGet(keys: string[]): Promise<[string, string][]>; +export function multiGet(keys: string[]): Promise<[string, unknown][]>; export type MultiGet = typeof multiGet; export function multiSet(keyValues: [string, unknown][]): void; diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 4deee12800f..cd4e85e42bf 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -1361,6 +1361,9 @@ handlers['save-global-prefs'] = async function (prefs) { prefs.serverSelfSignedCert, ); } + if ('ngrokConfig' in prefs) { + await asyncStorage.setItem('ngrokConfig', prefs.ngrokConfig); + } return 'ok'; }; @@ -1373,6 +1376,7 @@ handlers['load-global-prefs'] = async function () { [, theme], [, preferredDarkTheme], [, serverSelfSignedCert], + [, ngrokConfig], ] = await asyncStorage.multiGet([ 'floating-sidebar', 'max-months', @@ -1381,6 +1385,7 @@ handlers['load-global-prefs'] = async function () { 'theme', 'preferred-dark-theme', 'server-self-signed-cert', + 'ngrokConfig', ]); return { floatingSidebar: floatingSidebar === 'true' ? true : false, @@ -1400,6 +1405,7 @@ handlers['load-global-prefs'] = async function () { ? preferredDarkTheme : 'dark', serverSelfSignedCert: serverSelfSignedCert || undefined, + ngrokConfig: ngrokConfig || undefined, }; }; diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index 97610ce05d8..30f420e27be 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -81,6 +81,13 @@ export type GlobalPrefs = Partial<{ preferredDarkTheme: DarkTheme; documentDir: string; // Electron only serverSelfSignedCert: string; // Electron only + ngrokConfig?: { + // Electron only + autoStart?: boolean; + authToken?: string; + port?: number; + domain?: string; + }; }>; export type AuthMethods = 'password' | 'openid'; diff --git a/packages/loot-core/typings/window.d.ts b/packages/loot-core/typings/window.d.ts index 3a63353e776..05b072fbdf2 100644 --- a/packages/loot-core/typings/window.d.ts +++ b/packages/loot-core/typings/window.d.ts @@ -1,3 +1,5 @@ +import type { GlobalPrefs } from 'loot-core/types/prefs'; + export {}; declare global { @@ -6,6 +8,11 @@ declare global { IS_FAKE_WEB: boolean; ACTUAL_VERSION: string; openURLInBrowser: (url: string) => void; + downloadActualServer: (releaseVersion: string) => Promise<void>; + startActualServer: (releaseVersion: string) => Promise<void>; + exposeActualServer: ( + settings: GlobalPrefs['ngrokConfig'], + ) => Promise<{ url?: string; error?: string } | undefined>; saveFile: ( contents: string | Buffer, filename: string, diff --git a/packages/sync-server/.dockerignore b/packages/sync-server/.dockerignore new file mode 100644 index 00000000000..9a3b7d67742 --- /dev/null +++ b/packages/sync-server/.dockerignore @@ -0,0 +1,12 @@ +node_modules +user-files +server-files + +# Yarn +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/packages/sync-server/.editorconfig b/packages/sync-server/.editorconfig new file mode 100644 index 00000000000..551e30b9204 --- /dev/null +++ b/packages/sync-server/.editorconfig @@ -0,0 +1,11 @@ +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/packages/sync-server/.eslintignore b/packages/sync-server/.eslintignore new file mode 100644 index 00000000000..f4572de01e7 --- /dev/null +++ b/packages/sync-server/.eslintignore @@ -0,0 +1,6 @@ +**/node_modules/* +**/log/* +**/shared/* +/build + +supervise diff --git a/packages/sync-server/.eslintrc.cjs b/packages/sync-server/.eslintrc.cjs new file mode 100644 index 00000000000..c5538530206 --- /dev/null +++ b/packages/sync-server/.eslintrc.cjs @@ -0,0 +1,21 @@ +module.exports = { + root: true, + env: { + browser: true, + amd: true, + node: true, + jest: true + }, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'prettier'], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + rules: { + 'prettier/prettier': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_' + } + ] + } +}; diff --git a/packages/sync-server/.gitignore b/packages/sync-server/.gitignore new file mode 100644 index 00000000000..ad1e69fb0b1 --- /dev/null +++ b/packages/sync-server/.gitignore @@ -0,0 +1,33 @@ +.DS_Store +.#* +config.json +node_modules +log +supervise +bin/large-sync-data.txt +user-files +server-files +test-user-files +test-server-files +fly.toml +build/ +*.crt +*.pem +*.key +artifacts.json +.migrate +.migrate-test + +# Yarn +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +dist +.idea +/coverage +/coverage-e2e diff --git a/packages/sync-server/.node-version b/packages/sync-server/.node-version new file mode 100644 index 00000000000..e6db45a9079 --- /dev/null +++ b/packages/sync-server/.node-version @@ -0,0 +1 @@ +18.14.0 diff --git a/packages/sync-server/.prettierrc.json b/packages/sync-server/.prettierrc.json new file mode 100644 index 00000000000..a20502b7f06 --- /dev/null +++ b/packages/sync-server/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/packages/sync-server/.yarnrc.yml b/packages/sync-server/.yarnrc.yml new file mode 100644 index 00000000000..fd5296c36ec --- /dev/null +++ b/packages/sync-server/.yarnrc.yml @@ -0,0 +1,3 @@ +nodeLinker: node-modules + +yarnPath: .yarn/releases/yarn-4.3.1.cjs diff --git a/packages/sync-server/Dockerfile b/packages/sync-server/Dockerfile new file mode 100644 index 00000000000..028f514f9b8 --- /dev/null +++ b/packages/sync-server/Dockerfile @@ -0,0 +1,26 @@ +FROM node:18-bookworm as base +RUN apt-get update && apt-get install -y openssl +WORKDIR /app +COPY .yarn ./.yarn +COPY yarn.lock package.json .yarnrc.yml ./ +RUN yarn workspaces focus --all --production + +FROM node:18-bookworm-slim as prod +RUN apt-get update && apt-get install tini && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +ARG USERNAME=actual +ARG USER_UID=1001 +ARG USER_GID=$USER_UID +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME +RUN mkdir /data && chown -R ${USERNAME}:${USERNAME} /data + +WORKDIR /app +ENV NODE_ENV production +COPY --from=base /app/node_modules /app/node_modules +COPY package.json app.js ./ +COPY src ./src +COPY migrations ./migrations +ENTRYPOINT ["/usr/bin/tini","-g", "--"] +EXPOSE 5006 +CMD ["node", "app.js"] diff --git a/packages/sync-server/LICENSE.txt b/packages/sync-server/LICENSE.txt new file mode 100644 index 00000000000..3898f370d3c --- /dev/null +++ b/packages/sync-server/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright James Long + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/sync-server/README.md b/packages/sync-server/README.md new file mode 100644 index 00000000000..b1474e4d2d5 --- /dev/null +++ b/packages/sync-server/README.md @@ -0,0 +1,17 @@ +This is the main project to run [Actual](https://github.com/actualbudget/actual), a local-first personal finance tool. It comes with the latest version of Actual, and a server to persist changes and make data available across all devices. + +### Getting Started +Actual is a local-first personal finance tool. It is 100% free and open-source, written in NodeJS, it has a synchronization element so that all your changes can move between devices without any heavy lifting. + +If you are interested in contributing, or want to know how development works, see our [contributing](https://actualbudget.org/docs/contributing/) document we would love to have you. + +Want to say thanks? Click the ⭐ at the top of the page. + +### Documentation + +We have a wide range of documentation on how to use Actual. This is all available in our [Community Documentation](https://actualbudget.org/docs/), including topics on [installing](https://actualbudget.org/docs/install/), [Budgeting](https://actualbudget.org/docs/budgeting/), [Account Management](https://actualbudget.org/docs/accounts/), [Tips & Tricks](https://actualbudget.org/docs/getting-started/tips-tricks) and some documentation for developers. + +### Feature Requests +Current feature requests can be seen [here](https://github.com/actualbudget/actual/issues?q=is%3Aissue+label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc). Vote for your favorite requests by reacting 👍 to the top comment of the request. + +To add new feature requests, open a new Issue of the "Feature Request" type. diff --git a/packages/sync-server/app.js b/packages/sync-server/app.js new file mode 100644 index 00000000000..d0647fb88fd --- /dev/null +++ b/packages/sync-server/app.js @@ -0,0 +1,11 @@ +import runMigrations from './src/migrations.js'; + +runMigrations() + .then(() => { + //import the app here becasue initial migrations need to be run first - they are dependencies of the app.js + import('./src/app.js').then((app) => app.default()); // run the app + }) + .catch((err) => { + console.log('Error starting app:', err); + process.exit(1); + }); diff --git a/packages/sync-server/babel.config.json b/packages/sync-server/babel.config.json new file mode 100644 index 00000000000..e15ac017a2e --- /dev/null +++ b/packages/sync-server/babel.config.json @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-typescript"] +} diff --git a/packages/sync-server/docker-compose.yml b/packages/sync-server/docker-compose.yml new file mode 100644 index 00000000000..424844e4a10 --- /dev/null +++ b/packages/sync-server/docker-compose.yml @@ -0,0 +1,22 @@ +services: + actual_server: + image: docker.io/actualbudget/actual-server:latest + ports: + # This line makes Actual available at port 5006 of the device you run the server on, + # i.e. http://localhost:5006. You can change the first number to change the port, if you want. + - '5006:5006' + environment: + # Uncomment any of the lines below to set configuration options. + # - ACTUAL_HTTPS_KEY=/data/selfhost.key + # - ACTUAL_HTTPS_CERT=/data/selfhost.crt + # - ACTUAL_PORT=5006 + # - ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB=20 + # - ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB=50 + # - ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB=20 + # See all options and more details at https://actualbudget.github.io/docs/Installing/Configuration + # !! If you are not using any of these options, remove the 'environment:' tag entirely. + volumes: + # Change './actual-data' below to the path to the folder you want Actual to store its data in on your server. + # '/data' is the path Actual will look for its files in by default, so leave that as-is. + - ./actual-data:/data + restart: unless-stopped diff --git a/packages/sync-server/docker/download-artifacts.sh b/packages/sync-server/docker/download-artifacts.sh new file mode 100644 index 00000000000..e378954efd7 --- /dev/null +++ b/packages/sync-server/docker/download-artifacts.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +URL="https://api.github.com/repos/actualbudget/actual/actions/artifacts?name=actual-web&per_page=100" + +if [ -n "$GITHUB_TOKEN" ]; then + curl -L -o artifacts.json --header "Authorization: Bearer ${GITHUB_TOKEN}" $URL +else + curl -L -o artifacts.json $URL +fi + +if [ $? -ne 0 ]; then + echo "Failed to download artifacts.json" + exit 1 +fi diff --git a/packages/sync-server/docker/edge-alpine.Dockerfile b/packages/sync-server/docker/edge-alpine.Dockerfile new file mode 100644 index 00000000000..813f36b22f7 --- /dev/null +++ b/packages/sync-server/docker/edge-alpine.Dockerfile @@ -0,0 +1,37 @@ +FROM alpine:3.18 AS base +RUN apk add --no-cache nodejs yarn npm python3 openssl build-base jq curl +WORKDIR /app +COPY .yarn ./.yarn +COPY yarn.lock package.json .yarnrc.yml ./ +RUN if [ "$(uname -m)" = "armv7l" ]; then yarn config set taskPoolConcurrency 2; yarn config set networkConcurrency 5; fi +RUN yarn workspaces focus --all --production +RUN if [ "$(uname -m)" = "armv7l" ]; then npm install bcrypt better-sqlite3 --build-from-source; fi + +RUN mkdir /public +COPY artifacts.json /tmp/artifacts.json +RUN jq -r '[.artifacts[] | select(.workflow_run.head_branch == "master" and .workflow_run.head_repository_id == .workflow_run.repository_id)][0]' /tmp/artifacts.json > /tmp/latest-build.json + +ARG GITHUB_TOKEN +RUN curl -L -o /tmp/desktop-client.zip --header "Authorization: Bearer ${GITHUB_TOKEN}" $(jq -r '.archive_download_url' /tmp/latest-build.json) +RUN unzip /tmp/desktop-client.zip -d /public + +FROM alpine:3.18 AS prod +RUN apk add --no-cache nodejs tini + +ARG USERNAME=actual +ARG USER_UID=1001 +ARG USER_GID=$USER_UID +RUN addgroup -S ${USERNAME} -g ${USER_GID} && adduser -S ${USERNAME} -G ${USERNAME} -u ${USER_UID} +RUN mkdir /data && chown -R ${USERNAME}:${USERNAME} /data + +WORKDIR /app +ENV NODE_ENV production +COPY --from=base /app/node_modules /app/node_modules +COPY --from=base /public /public +COPY package.json app.js ./ +COPY src ./src +COPY migrations ./migrations +ENTRYPOINT ["/sbin/tini","-g", "--"] +ENV ACTUAL_WEB_ROOT=/public +EXPOSE 5006 +CMD ["node", "app.js"] diff --git a/packages/sync-server/docker/edge-ubuntu.Dockerfile b/packages/sync-server/docker/edge-ubuntu.Dockerfile new file mode 100644 index 00000000000..e45e9f4dd33 --- /dev/null +++ b/packages/sync-server/docker/edge-ubuntu.Dockerfile @@ -0,0 +1,37 @@ +FROM node:18-bookworm AS base +RUN apt-get update && apt-get install -y openssl jq +WORKDIR /app +COPY .yarn ./.yarn +COPY yarn.lock package.json .yarnrc.yml ./ +RUN if [ "$(uname -m)" = "armv7l" ]; then yarn config set taskPoolConcurrency 2; yarn config set networkConcurrency 5; fi +RUN yarn workspaces focus --all --production + +RUN mkdir /public +COPY artifacts.json /tmp/artifacts.json +RUN jq -r '[.artifacts[] | select(.workflow_run.head_branch == "master" and .workflow_run.head_repository_id == .workflow_run.repository_id)][0]' /tmp/artifacts.json > /tmp/latest-build.json + +ARG GITHUB_TOKEN +RUN curl -L -o /tmp/desktop-client.zip --header "Authorization: Bearer ${GITHUB_TOKEN}" $(jq -r '.archive_download_url' /tmp/latest-build.json) +RUN unzip /tmp/desktop-client.zip -d /public + +FROM node:18-bookworm-slim AS prod +RUN apt-get update && apt-get install tini && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +ARG USERNAME=actual +ARG USER_UID=1001 +ARG USER_GID=$USER_UID +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME +RUN mkdir /data && chown -R ${USERNAME}:${USERNAME} /data + +WORKDIR /app +ENV NODE_ENV production +COPY --from=base /app/node_modules /app/node_modules +COPY --from=base /public /public +COPY package.json app.js ./ +COPY src ./src +COPY migrations ./migrations +ENTRYPOINT ["/usr/bin/tini","-g", "--"] +ENV ACTUAL_WEB_ROOT=/public +EXPOSE 5006 +CMD ["node", "app.js"] diff --git a/packages/sync-server/docker/stable-alpine.Dockerfile b/packages/sync-server/docker/stable-alpine.Dockerfile new file mode 100644 index 00000000000..8ff468bada3 --- /dev/null +++ b/packages/sync-server/docker/stable-alpine.Dockerfile @@ -0,0 +1,27 @@ +FROM alpine:3.18 AS base +RUN apk add --no-cache nodejs yarn npm python3 openssl build-base +WORKDIR /app +COPY .yarn ./.yarn +COPY yarn.lock package.json .yarnrc.yml ./ +RUN if [ "$(uname -m)" = "armv7l" ]; then yarn config set taskPoolConcurrency 2; yarn config set networkConcurrency 5; fi +RUN yarn workspaces focus --all --production +RUN if [ "$(uname -m)" = "armv7l" ]; then npm install bcrypt better-sqlite3 --build-from-source; fi + +FROM alpine:3.18 AS prod +RUN apk add --no-cache nodejs tini + +ARG USERNAME=actual +ARG USER_UID=1001 +ARG USER_GID=$USER_UID +RUN addgroup -S ${USERNAME} -g ${USER_GID} && adduser -S ${USERNAME} -G ${USERNAME} -u ${USER_UID} +RUN mkdir /data && chown -R ${USERNAME}:${USERNAME} /data + +WORKDIR /app +ENV NODE_ENV production +COPY --from=base /app/node_modules /app/node_modules +COPY package.json app.js ./ +COPY src ./src +COPY migrations ./migrations +ENTRYPOINT ["/sbin/tini","-g", "--"] +EXPOSE 5006 +CMD ["node", "app.js"] diff --git a/packages/sync-server/docker/stable-ubuntu.Dockerfile b/packages/sync-server/docker/stable-ubuntu.Dockerfile new file mode 100644 index 00000000000..6de38b509c4 --- /dev/null +++ b/packages/sync-server/docker/stable-ubuntu.Dockerfile @@ -0,0 +1,27 @@ +FROM node:18-bookworm AS base +RUN apt-get update && apt-get install -y openssl +WORKDIR /app +COPY .yarn ./.yarn +COPY yarn.lock package.json .yarnrc.yml ./ +RUN if [ "$(uname -m)" = "armv7l" ]; then yarn config set taskPoolConcurrency 2; yarn config set networkConcurrency 5; fi +RUN yarn workspaces focus --all --production + +FROM node:18-bookworm-slim AS prod +RUN apt-get update && apt-get install tini && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +ARG USERNAME=actual +ARG USER_UID=1001 +ARG USER_GID=$USER_UID +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME +RUN mkdir /data && chown -R ${USERNAME}:${USERNAME} /data + +WORKDIR /app +ENV NODE_ENV production +COPY --from=base /app/node_modules /app/node_modules +COPY package.json app.js ./ +COPY src ./src +COPY migrations ./migrations +ENTRYPOINT ["/usr/bin/tini","-g", "--"] +EXPOSE 5006 +CMD ["node", "app.js"] diff --git a/packages/sync-server/jest.config.json b/packages/sync-server/jest.config.json new file mode 100644 index 00000000000..f33061a9d76 --- /dev/null +++ b/packages/sync-server/jest.config.json @@ -0,0 +1,14 @@ +{ + "globalSetup": "./jest.global-setup.js", + "globalTeardown": "./jest.global-teardown.js", + "testPathIgnorePatterns": ["dist", "/node_modules/", "/build/"], + "roots": ["<rootDir>"], + "moduleFileExtensions": ["ts", "js", "json"], + "testEnvironment": "node", + "collectCoverage": true, + "collectCoverageFrom": ["**/*.{js,ts,tsx}"], + "coveragePathIgnorePatterns": ["dist", "/node_modules/", "/build/", "/coverage/"], + "coverageReporters": ["html", "lcov", "text", "text-summary"], + "resetMocks": true, + "restoreMocks": true +} diff --git a/packages/sync-server/jest.global-setup.js b/packages/sync-server/jest.global-setup.js new file mode 100644 index 00000000000..524054dd5dd --- /dev/null +++ b/packages/sync-server/jest.global-setup.js @@ -0,0 +1,100 @@ +import getAccountDb from './src/account-db.js'; +import runMigrations from './src/migrations.js'; + +const GENERIC_ADMIN_ID = 'genericAdmin'; +const GENERIC_USER_ID = 'genericUser'; +const ADMIN_ROLE_ID = 'ADMIN'; +const BASIC_ROLE_ID = 'BASIC'; + +const createUser = (userId, userName, role, owner = 0, enabled = 1) => { + const missingParams = []; + if (!userId) missingParams.push('userId'); + if (!userName) missingParams.push('userName'); + if (!role) missingParams.push('role'); + if (missingParams.length > 0) { + throw new Error(`Missing required parameters: ${missingParams.join(', ')}`); + } + + if ( + typeof userId !== 'string' || + typeof userName !== 'string' || + typeof role !== 'string' + ) { + throw new Error( + 'Invalid parameter types. userId, userName, and role must be strings', + ); + } + + try { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)', + [userId, userName, userName, enabled, owner, role], + ); + } catch (error) { + console.error(`Error creating user ${userName}:`, error); + throw error; + } +}; + +const setSessionUser = (userId, token = 'valid-token') => { + if (!userId) { + throw new Error('userId is required'); + } + + try { + const db = getAccountDb(); + const session = db.first('SELECT token FROM sessions WHERE token = ?', [ + token, + ]); + if (!session) { + throw new Error(`Session not found for token: ${token}`); + } + + db.mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ + userId, + token, + ]); + } catch (error) { + console.error(`Error updating session for user ${userId}:`, error); + throw error; + } +}; + +export default async function setup() { + const NEVER_EXPIRES = -1; // or consider using a far future timestamp + + await runMigrations(); + + createUser(GENERIC_ADMIN_ID, 'admin', ADMIN_ROLE_ID, 1); + + // Insert a fake "valid-token" fixture that can be reused + const db = getAccountDb(); + try { + await db.mutate('BEGIN TRANSACTION'); + + await db.mutate('DELETE FROM sessions'); + await db.mutate( + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + ['valid-token', NEVER_EXPIRES, 'genericAdmin'], + ); + await db.mutate( + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + ['valid-token-admin', NEVER_EXPIRES, 'genericAdmin'], + ); + + await db.mutate( + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + ['valid-token-user', NEVER_EXPIRES, 'genericUser'], + ); + + await db.mutate('COMMIT'); + } catch (error) { + await db.mutate('ROLLBACK'); + throw new Error(`Failed to setup test sessions: ${error.message}`); + } + + setSessionUser('genericAdmin'); + setSessionUser('genericAdmin', 'valid-token-admin'); + + createUser(GENERIC_USER_ID, 'user', BASIC_ROLE_ID, 1); +} diff --git a/packages/sync-server/jest.global-teardown.js b/packages/sync-server/jest.global-teardown.js new file mode 100644 index 00000000000..4e19fc38547 --- /dev/null +++ b/packages/sync-server/jest.global-teardown.js @@ -0,0 +1,5 @@ +import runMigrations from './src/migrations.js'; + +export default async function teardown() { + await runMigrations('down'); +} diff --git a/packages/sync-server/migrations/1694360000000-create-folders.js b/packages/sync-server/migrations/1694360000000-create-folders.js new file mode 100644 index 00000000000..2ba62b8df4a --- /dev/null +++ b/packages/sync-server/migrations/1694360000000-create-folders.js @@ -0,0 +1,24 @@ +import fs from 'node:fs/promises'; +import config from '../src/load-config.js'; + +async function ensureExists(path) { + try { + await fs.mkdir(path); + } catch (err) { + if (err.code == 'EEXIST') { + return null; + } + + throw err; + } +} + +export const up = async function () { + await ensureExists(config.serverFiles); + await ensureExists(config.userFiles); +}; + +export const down = async function () { + await fs.rm(config.serverFiles, { recursive: true, force: true }); + await fs.rm(config.userFiles, { recursive: true, force: true }); +}; diff --git a/packages/sync-server/migrations/1694360479680-create-account-db.js b/packages/sync-server/migrations/1694360479680-create-account-db.js new file mode 100644 index 00000000000..fd57b79cc8d --- /dev/null +++ b/packages/sync-server/migrations/1694360479680-create-account-db.js @@ -0,0 +1,30 @@ +import getAccountDb from '../src/account-db.js'; + +export const up = async function () { + await getAccountDb().exec(` + CREATE TABLE IF NOT EXISTS auth + (password TEXT PRIMARY KEY); + + CREATE TABLE IF NOT EXISTS sessions + (token TEXT PRIMARY KEY); + + CREATE TABLE IF NOT EXISTS files + (id TEXT PRIMARY KEY, + group_id TEXT, + sync_version SMALLINT, + encrypt_meta TEXT, + encrypt_keyid TEXT, + encrypt_salt TEXT, + encrypt_test TEXT, + deleted BOOLEAN DEFAULT FALSE, + name TEXT); + `); +}; + +export const down = async function () { + await getAccountDb().exec(` + DROP TABLE auth; + DROP TABLE sessions; + DROP TABLE files; + `); +}; diff --git a/packages/sync-server/migrations/1694362247011-create-secret-table.js b/packages/sync-server/migrations/1694362247011-create-secret-table.js new file mode 100644 index 00000000000..2f60a081513 --- /dev/null +++ b/packages/sync-server/migrations/1694362247011-create-secret-table.js @@ -0,0 +1,16 @@ +import getAccountDb from '../src/account-db.js'; + +export const up = async function () { + await getAccountDb().exec(` + CREATE TABLE IF NOT EXISTS secrets ( + name TEXT PRIMARY KEY, + value BLOB + ); + `); +}; + +export const down = async function () { + await getAccountDb().exec(` + DROP TABLE secrets; + `); +}; diff --git a/packages/sync-server/migrations/1702667624000-rename-nordigen-secrets.js b/packages/sync-server/migrations/1702667624000-rename-nordigen-secrets.js new file mode 100644 index 00000000000..5dcaff73574 --- /dev/null +++ b/packages/sync-server/migrations/1702667624000-rename-nordigen-secrets.js @@ -0,0 +1,19 @@ +import getAccountDb from '../src/account-db.js'; + +export const up = async function () { + await getAccountDb().exec( + `UPDATE secrets SET name = 'gocardless_secretId' WHERE name = 'nordigen_secretId'`, + ); + await getAccountDb().exec( + `UPDATE secrets SET name = 'gocardless_secretKey' WHERE name = 'nordigen_secretKey'`, + ); +}; + +export const down = async function () { + await getAccountDb().exec( + `UPDATE secrets SET name = 'nordigen_secretId' WHERE name = 'gocardless_secretId'`, + ); + await getAccountDb().exec( + `UPDATE secrets SET name = 'nordigen_secretKey' WHERE name = 'gocardless_secretKey'`, + ); +}; diff --git a/packages/sync-server/migrations/1718889148000-openid.js b/packages/sync-server/migrations/1718889148000-openid.js new file mode 100644 index 00000000000..b170aea05dd --- /dev/null +++ b/packages/sync-server/migrations/1718889148000-openid.js @@ -0,0 +1,41 @@ +import getAccountDb from '../src/account-db.js'; + +export const up = async function () { + await getAccountDb().exec( + ` + BEGIN TRANSACTION; + CREATE TABLE auth_new + (method TEXT PRIMARY KEY, + display_name TEXT, + extra_data TEXT, active INTEGER); + + INSERT INTO auth_new (method, display_name, extra_data, active) + SELECT 'password', 'Password', password, 1 FROM auth; + DROP TABLE auth; + ALTER TABLE auth_new RENAME TO auth; + + CREATE TABLE pending_openid_requests + (state TEXT PRIMARY KEY, + code_verifier TEXT, + return_url TEXT, + expiry_time INTEGER); + COMMIT;`, + ); +}; + +export const down = async function () { + await getAccountDb().exec( + ` + BEGIN TRANSACTION; + ALTER TABLE auth RENAME TO auth_temp; + CREATE TABLE auth + (password TEXT); + INSERT INTO auth (password) + SELECT extra_data FROM auth_temp WHERE method = 'password'; + DROP TABLE auth_temp; + + DROP TABLE pending_openid_requests; + COMMIT; + `, + ); +}; diff --git a/packages/sync-server/migrations/1719409568000-multiuser.js b/packages/sync-server/migrations/1719409568000-multiuser.js new file mode 100644 index 00000000000..228bcdc89da --- /dev/null +++ b/packages/sync-server/migrations/1719409568000-multiuser.js @@ -0,0 +1,115 @@ +import getAccountDb from '../src/account-db.js'; +import * as uuid from 'uuid'; + +export const up = async function () { + const accountDb = getAccountDb(); + + accountDb.transaction(() => { + accountDb.exec( + ` + CREATE TABLE users + (id TEXT PRIMARY KEY, + user_name TEXT, + display_name TEXT, + role TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + owner INTEGER NOT NULL DEFAULT 0); + + CREATE TABLE user_access + (user_id TEXT, + file_id TEXT, + PRIMARY KEY (user_id, file_id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (file_id) REFERENCES files(id) + ); + + ALTER TABLE files + ADD COLUMN owner TEXT; + + ALTER TABLE sessions + ADD COLUMN expires_at INTEGER; + + ALTER TABLE sessions + ADD COLUMN user_id TEXT; + + ALTER TABLE sessions + ADD COLUMN auth_method TEXT; + `, + ); + + const userId = uuid.v4(); + accountDb.mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', + [userId, '', '', 'ADMIN'], + ); + + accountDb.mutate( + 'UPDATE sessions SET user_id = ?, expires_at = ?, auth_method = ? WHERE auth_method IS NULL', + [userId, -1, 'password'], + ); + }); +}; + +export const down = async function () { + await getAccountDb().exec( + ` + BEGIN TRANSACTION; + + DROP TABLE IF EXISTS user_access; + + CREATE TABLE sessions_backup ( + token TEXT PRIMARY KEY + ); + + INSERT INTO sessions_backup (token) + SELECT token FROM sessions; + + DROP TABLE sessions; + + ALTER TABLE sessions_backup RENAME TO sessions; + + CREATE TABLE files_backup ( + id TEXT PRIMARY KEY, + group_id TEXT, + sync_version SMALLINT, + encrypt_meta TEXT, + encrypt_keyid TEXT, + encrypt_salt TEXT, + encrypt_test TEXT, + deleted BOOLEAN DEFAULT FALSE, + name TEXT + ); + + INSERT INTO files_backup ( + id, + group_id, + sync_version, + encrypt_meta, + encrypt_keyid, + encrypt_salt, + encrypt_test, + deleted, + name + ) + SELECT + id, + group_id, + sync_version, + encrypt_meta, + encrypt_keyid, + encrypt_salt, + encrypt_test, + deleted, + name + FROM files; + + DROP TABLE files; + + ALTER TABLE files_backup RENAME TO files; + + DROP TABLE IF EXISTS users; + + COMMIT; + `, + ); +}; diff --git a/packages/sync-server/package.json b/packages/sync-server/package.json new file mode 100644 index 00000000000..9f40c826446 --- /dev/null +++ b/packages/sync-server/package.json @@ -0,0 +1,69 @@ +{ + "name": "actual-sync", + "version": "25.1.0", + "license": "MIT", + "description": "actual syncing server", + "type": "module", + "scripts": { + "start": "node app", + "lint": "eslint . --max-warnings 0", + "lint:fix": "eslint . --fix", + "build": "tsc", + "test": "NODE_ENV=test NODE_OPTIONS='--experimental-vm-modules --trace-warnings' jest --coverage", + "db:migrate": "NODE_ENV=development node src/run-migrations.js up", + "db:downgrade": "NODE_ENV=development node src/run-migrations.js down", + "db:test-migrate": "NODE_ENV=test node src/run-migrations.js up", + "db:test-downgrade": "NODE_ENV=test node src/run-migrations.js down", + "types": "tsc --noEmit --incremental", + "verify": "yarn lint && yarn types", + "reset-password": "node src/scripts/reset-password.js", + "enable-openid": "node src/scripts/enable-openid.js", + "disable-openid": "node src/scripts/disable-openid.js", + "health-check": "node src/scripts/health-check.js" + }, + "dependencies": { + "@actual-app/crdt": "*", + "@actual-app/web": "*", + "bcrypt": "^5.1.1", + "better-sqlite3": "^11.7.0", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "date-fns": "^2.30.0", + "debug": "^4.3.4", + "express": "4.20.0", + "express-actuator": "1.8.4", + "express-rate-limit": "^6.7.0", + "express-response-size": "^0.0.3", + "express-winston": "^4.2.0", + "jws": "^4.0.0", + "migrate": "^2.0.1", + "nordigen-node": "^1.4.0", + "openid-client": "^5.4.2", + "uuid": "^9.0.0", + "winston": "^3.14.2" + }, + "devDependencies": { + "@babel/preset-typescript": "^7.20.2", + "@types/bcrypt": "^5.0.2", + "@types/better-sqlite3": "^7.6.12", + "@types/cors": "^2.8.13", + "@types/express": "^4.17.17", + "@types/express-actuator": "^1.8.0", + "@types/jest": "^29.2.3", + "@types/node": "^17.0.45", + "@types/supertest": "^2.0.12", + "@types/uuid": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "eslint": "^8.33.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.3.1", + "prettier": "^2.8.3", + "supertest": "^6.3.1", + "typescript": "^4.9.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "packageManager": "yarn@4.3.1" +} diff --git a/packages/sync-server/src/account-db.js b/packages/sync-server/src/account-db.js new file mode 100644 index 00000000000..c7fff76ed59 --- /dev/null +++ b/packages/sync-server/src/account-db.js @@ -0,0 +1,223 @@ +import { join } from 'node:path'; +import openDatabase from './db.js'; +import config from './load-config.js'; +import * as bcrypt from 'bcrypt'; +import { bootstrapPassword, loginWithPassword } from './accounts/password.js'; +import { bootstrapOpenId } from './accounts/openid.js'; + +let _accountDb; + +export default function getAccountDb() { + if (_accountDb === undefined) { + const dbPath = join(config.serverFiles, 'account.sqlite'); + _accountDb = openDatabase(dbPath); + } + + return _accountDb; +} + +export function needsBootstrap() { + let accountDb = getAccountDb(); + let rows = accountDb.all('SELECT * FROM auth'); + return rows.length === 0; +} + +export function listLoginMethods() { + let accountDb = getAccountDb(); + let rows = accountDb.all('SELECT method, display_name, active FROM auth'); + return rows.map((r) => ({ + method: r.method, + active: r.active, + displayName: r.display_name, + })); +} + +export function getActiveLoginMethod() { + let accountDb = getAccountDb(); + let { method } = + accountDb.first('SELECT method FROM auth WHERE active = 1') || {}; + return method; +} + +/* + * Get the Login Method in the following order + * req (the frontend can say which method in the case it wants to resort to forcing password auth) + * config options + * fall back to using password + */ +export function getLoginMethod(req) { + if ( + typeof req !== 'undefined' && + (req.body || { loginMethod: null }).loginMethod + ) { + return req.body.loginMethod; + } + + const activeMethod = getActiveLoginMethod(); + + return config.loginMethod || activeMethod || 'password'; +} + +export async function bootstrap(loginSettings) { + if (!loginSettings) { + return { error: 'invalid-login-settings' }; + } + const passEnabled = 'password' in loginSettings; + const openIdEnabled = 'openId' in loginSettings; + + const accountDb = getAccountDb(); + accountDb.mutate('BEGIN TRANSACTION'); + try { + const { countOfOwner } = + accountDb.first( + `SELECT count(*) as countOfOwner + FROM users + WHERE users.user_name <> '' and users.owner = 1`, + ) || {}; + + if (!openIdEnabled || countOfOwner > 0) { + if (!needsBootstrap()) { + accountDb.mutate('ROLLBACK'); + return { error: 'already-bootstrapped' }; + } + } + + if (!passEnabled && !openIdEnabled) { + accountDb.mutate('ROLLBACK'); + return { error: 'no-auth-method-selected' }; + } + + if (passEnabled && openIdEnabled) { + accountDb.mutate('ROLLBACK'); + return { error: 'max-one-method-allowed' }; + } + + if (passEnabled) { + let { error } = bootstrapPassword(loginSettings.password); + if (error) { + accountDb.mutate('ROLLBACK'); + return { error }; + } + } + + if (openIdEnabled) { + let { error } = await bootstrapOpenId(loginSettings.openId); + if (error) { + accountDb.mutate('ROLLBACK'); + return { error }; + } + } + + accountDb.mutate('COMMIT'); + return passEnabled ? loginWithPassword(loginSettings.password) : {}; + } catch (error) { + accountDb.mutate('ROLLBACK'); + throw error; + } +} + +export function isAdmin(userId) { + return hasPermission(userId, 'ADMIN'); +} + +export function hasPermission(userId, permission) { + return getUserPermission(userId) === permission; +} + +export async function enableOpenID(loginSettings) { + if (!loginSettings || !loginSettings.openId) { + return { error: 'invalid-login-settings' }; + } + + let { error } = (await bootstrapOpenId(loginSettings.openId)) || {}; + if (error) { + return { error }; + } + + getAccountDb().mutate('DELETE FROM sessions'); +} + +export async function disableOpenID(loginSettings) { + if (!loginSettings || !loginSettings.password) { + return { error: 'invalid-login-settings' }; + } + + let accountDb = getAccountDb(); + const { extra_data: passwordHash } = + accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [ + 'password', + ]) || {}; + + if (!passwordHash) { + return { error: 'invalid-password' }; + } + + if (!loginSettings?.password) { + return { error: 'invalid-password' }; + } + + if (passwordHash) { + let confirmed = bcrypt.compareSync(loginSettings.password, passwordHash); + + if (!confirmed) { + return { error: 'invalid-password' }; + } + } + + let { error } = (await bootstrapPassword(loginSettings.password)) || {}; + if (error) { + return { error }; + } + + try { + accountDb.transaction(() => { + accountDb.mutate('DELETE FROM sessions'); + accountDb.mutate( + `DELETE FROM user_access + WHERE user_access.user_id IN ( + SELECT users.id + FROM users + WHERE users.user_name <> ? + );`, + [''], + ); + accountDb.mutate('DELETE FROM users WHERE user_name <> ?', ['']); + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); + }); + } catch (err) { + console.error('Error cleaning up openid information:', err); + return { error: 'database-error' }; + } +} + +export function getSession(token) { + let accountDb = getAccountDb(); + return accountDb.first('SELECT * FROM sessions WHERE token = ?', [token]); +} + +export function getUserInfo(userId) { + let accountDb = getAccountDb(); + return accountDb.first('SELECT * FROM users WHERE id = ?', [userId]); +} + +export function getUserPermission(userId) { + let accountDb = getAccountDb(); + const { role } = accountDb.first( + `SELECT role FROM users + WHERE users.id = ?`, + [userId], + ) || { role: '' }; + + return role; +} + +export function clearExpiredSessions() { + const clearThreshold = Math.floor(Date.now() / 1000) - 3600; + + const deletedSessions = getAccountDb().mutate( + 'DELETE FROM sessions WHERE expires_at <> -1 and expires_at < ?', + [clearThreshold], + ).changes; + + console.log(`Deleted ${deletedSessions} old sessions`); +} diff --git a/packages/sync-server/src/accounts/openid.js b/packages/sync-server/src/accounts/openid.js new file mode 100644 index 00000000000..2080a3bcbb4 --- /dev/null +++ b/packages/sync-server/src/accounts/openid.js @@ -0,0 +1,329 @@ +import getAccountDb, { clearExpiredSessions } from '../account-db.js'; +import * as uuid from 'uuid'; +import { generators, Issuer } from 'openid-client'; +import finalConfig from '../load-config.js'; +import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js'; +import { + getUserByUsername, + transferAllFilesFromUser, +} from '../services/user-service.js'; + +export async function bootstrapOpenId(config) { + if (!('issuer' in config)) { + return { error: 'missing-issuer' }; + } + if (!('client_id' in config)) { + return { error: 'missing-client-id' }; + } + if (!('client_secret' in config)) { + return { error: 'missing-client-secret' }; + } + if (!('server_hostname' in config)) { + return { error: 'missing-server-hostname' }; + } + + try { + await setupOpenIdClient(config); + } catch (err) { + console.error('Error setting up OpenID client:', err); + return { error: 'configuration-error' }; + } + + let accountDb = getAccountDb(); + try { + accountDb.transaction(() => { + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); + accountDb.mutate('UPDATE auth SET active = 0'); + accountDb.mutate( + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", + [JSON.stringify(config)], + ); + }); + } catch (err) { + console.error('Error updating auth table:', err); + return { error: 'database-error' }; + } + + return {}; +} + +async function setupOpenIdClient(config) { + let issuer = + typeof config.issuer === 'string' + ? await Issuer.discover(config.issuer) + : new Issuer({ + issuer: config.issuer.name, + authorization_endpoint: config.issuer.authorization_endpoint, + token_endpoint: config.issuer.token_endpoint, + userinfo_endpoint: config.issuer.userinfo_endpoint, + }); + + const client = new issuer.Client({ + client_id: config.client_id, + client_secret: config.client_secret, + redirect_uri: new URL( + '/openid/callback', + config.server_hostname, + ).toString(), + validate_id_token: true, + }); + + return client; +} + +export async function loginWithOpenIdSetup(returnUrl) { + if (!returnUrl) { + return { error: 'return-url-missing' }; + } + if (!isValidRedirectUrl(returnUrl)) { + return { error: 'invalid-return-url' }; + } + + let accountDb = getAccountDb(); + let config = accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [ + 'openid', + ]); + if (!config) { + return { error: 'openid-not-configured' }; + } + + try { + config = JSON.parse(config['extra_data']); + } catch (err) { + console.error('Error parsing OpenID configuration:', err); + return { error: 'openid-setup-failed' }; + } + + let client; + try { + client = await setupOpenIdClient(config); + } catch (err) { + console.error('Error setting up OpenID client:', err); + return { error: 'openid-setup-failed' }; + } + + const state = generators.state(); + const code_verifier = generators.codeVerifier(); + const code_challenge = generators.codeChallenge(code_verifier); + + const now_time = Date.now(); + const expiry_time = now_time + 300 * 1000; + + accountDb.mutate( + 'DELETE FROM pending_openid_requests WHERE expiry_time < ?', + [now_time], + ); + accountDb.mutate( + 'INSERT INTO pending_openid_requests (state, code_verifier, return_url, expiry_time) VALUES (?, ?, ?, ?)', + [state, code_verifier, returnUrl, expiry_time], + ); + + const url = client.authorizationUrl({ + response_type: 'code', + scope: 'openid email profile', + state, + code_challenge, + code_challenge_method: 'S256', + }); + + return { url }; +} + +export async function loginWithOpenIdFinalize(body) { + if (!body.code) { + return { error: 'missing-authorization-code' }; + } + if (!body.state) { + return { error: 'missing-state' }; + } + + let accountDb = getAccountDb(); + let config = accountDb.first( + "SELECT extra_data FROM auth WHERE method = 'openid' AND active = 1", + ); + if (!config) { + return { error: 'openid-not-configured' }; + } + try { + config = JSON.parse(config['extra_data']); + } catch (err) { + console.error('Error parsing OpenID configuration:', err); + return { error: 'openid-setup-failed' }; + } + let client; + try { + client = await setupOpenIdClient(config); + } catch (err) { + console.error('Error setting up OpenID client:', err); + return { error: 'openid-setup-failed' }; + } + + let pendingRequest = accountDb.first( + 'SELECT code_verifier, return_url FROM pending_openid_requests WHERE state = ? AND expiry_time > ?', + [body.state, Date.now()], + ); + + if (!pendingRequest) { + return { error: 'invalid-or-expired-state' }; + } + + let { code_verifier, return_url } = pendingRequest; + + try { + let tokenSet = null; + + if (!config.authMethod || config.authMethod === 'openid') { + const params = { code: body.code, state: body.state }; + tokenSet = await client.callback(client.redirect_uris[0], params, { + code_verifier, + state: body.state, + }); + } else { + tokenSet = await client.grant({ + grant_type: 'authorization_code', + code: body.code, + redirect_uri: client.redirect_uris[0], + code_verifier, + }); + } + const userInfo = await client.userinfo(tokenSet.access_token); + const identity = + userInfo.preferred_username ?? + userInfo.login ?? + userInfo.email ?? + userInfo.id ?? + userInfo.name ?? + 'default-username'; + if (identity == null) { + return { error: 'openid-grant-failed: no identification was found' }; + } + + let userId = null; + try { + accountDb.transaction(() => { + let { countUsersWithUserName } = accountDb.first( + 'SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', + [''], + ); + if (countUsersWithUserName === 0) { + userId = uuid.v4(); + // Check if user was created by another transaction + const existingUser = accountDb.first( + 'SELECT id FROM users WHERE user_name = ?', + [identity], + ); + if (existingUser) { + throw new Error('user-already-exists'); + } + accountDb.mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', + [ + userId, + identity, + userInfo.name ?? userInfo.email ?? identity, + 'ADMIN', + ], + ); + + const userFromPasswordMethod = getUserByUsername(''); + if (userFromPasswordMethod) { + transferAllFilesFromUser(userId, userFromPasswordMethod.user_id); + } + } else { + let { id: userIdFromDb, display_name: displayName } = + accountDb.first( + 'SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1', + [identity], + ) || {}; + + if (userIdFromDb == null) { + throw new Error('openid-grant-failed'); + } + + if (!displayName && userInfo.name) { + accountDb.mutate('UPDATE users set display_name = ? WHERE id = ?', [ + userInfo.name, + userIdFromDb, + ]); + } + + userId = userIdFromDb; + } + }); + } catch (error) { + if (error.message === 'user-already-exists') { + return { error: 'user-already-exists' }; + } else if (error.message === 'openid-grant-failed') { + return { error: 'openid-grant-failed' }; + } else { + throw error; // Re-throw other unexpected errors + } + } + + const token = uuid.v4(); + + let expiration; + if (finalConfig.token_expiration === 'openid-provider') { + expiration = tokenSet.expires_at ?? TOKEN_EXPIRATION_NEVER; + } else if (finalConfig.token_expiration === 'never') { + expiration = TOKEN_EXPIRATION_NEVER; + } else if (typeof finalConfig.token_expiration === 'number') { + expiration = + Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60; + } else { + expiration = Math.floor(Date.now() / 1000) + 10 * 60; + } + + accountDb.mutate( + 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', + [token, expiration, userId, 'openid'], + ); + + clearExpiredSessions(); + + return { url: `${return_url}/openid-cb?token=${token}` }; + } catch (err) { + console.error('OpenID grant failed:', err); + return { error: 'openid-grant-failed' }; + } +} + +export function getServerHostname() { + const auth = getAccountDb().first( + 'select * from auth WHERE method = ? and active = 1', + ['openid'], + ); + if (auth && auth.extra_data) { + try { + const openIdConfig = JSON.parse(auth.extra_data); + return openIdConfig.server_hostname; + } catch (error) { + console.error('Error parsing OpenID configuration:', error); + } + } + return null; +} + +export function isValidRedirectUrl(url) { + const serverHostname = getServerHostname(); + + if (!serverHostname) { + return false; + } + + try { + const redirectUrl = new URL(url); + const serverUrl = new URL(serverHostname); + + if ( + redirectUrl.hostname === serverUrl.hostname || + redirectUrl.hostname === 'localhost' + ) { + return true; + } else { + return false; + } + } catch (err) { + return false; + } +} diff --git a/packages/sync-server/src/accounts/password.js b/packages/sync-server/src/accounts/password.js new file mode 100644 index 00000000000..e841697a7f7 --- /dev/null +++ b/packages/sync-server/src/accounts/password.js @@ -0,0 +1,124 @@ +import * as bcrypt from 'bcrypt'; +import getAccountDb, { clearExpiredSessions } from '../account-db.js'; +import * as uuid from 'uuid'; +import finalConfig from '../load-config.js'; +import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js'; + +function isValidPassword(password) { + return password != null && password !== ''; +} + +function hashPassword(password) { + return bcrypt.hashSync(password, 12); +} + +export function bootstrapPassword(password) { + if (!isValidPassword(password)) { + return { error: 'invalid-password' }; + } + + let hashed = hashPassword(password); + let accountDb = getAccountDb(); + accountDb.transaction(() => { + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['password']); + accountDb.mutate('UPDATE auth SET active = 0'); + accountDb.mutate( + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('password', 'Password', ?, 1)", + [hashed], + ); + }); + + return {}; +} + +export function loginWithPassword(password) { + if (!isValidPassword(password)) { + return { error: 'invalid-password' }; + } + + let accountDb = getAccountDb(); + const { extra_data: passwordHash } = + accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [ + 'password', + ]) || {}; + + if (!passwordHash) { + return { error: 'invalid-password' }; + } + + let confirmed = bcrypt.compareSync(password, passwordHash); + + if (!confirmed) { + return { error: 'invalid-password' }; + } + + let sessionRow = accountDb.first( + 'SELECT * FROM sessions WHERE auth_method = ?', + ['password'], + ); + + let token = sessionRow ? sessionRow.token : uuid.v4(); + + let { totalOfUsers } = accountDb.first( + 'SELECT count(*) as totalOfUsers FROM users', + ); + let userId = null; + if (totalOfUsers === 0) { + userId = uuid.v4(); + accountDb.mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', + [userId, '', '', 'ADMIN'], + ); + } else { + let { id: userIdFromDb } = accountDb.first( + 'SELECT id FROM users WHERE user_name = ?', + [''], + ); + + userId = userIdFromDb; + + if (!userId) { + return { error: 'user-not-found' }; + } + } + + let expiration = TOKEN_EXPIRATION_NEVER; + if ( + finalConfig.token_expiration != 'never' && + finalConfig.token_expiration != 'openid-provider' && + typeof finalConfig.token_expiration === 'number' + ) { + expiration = + Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60; + } + + if (!sessionRow) { + accountDb.mutate( + 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', + [token, expiration, userId, 'password'], + ); + } else { + accountDb.mutate( + 'UPDATE sessions SET user_id = ?, expires_at = ? WHERE token = ?', + [userId, expiration, token], + ); + } + + clearExpiredSessions(); + + return { token }; +} + +export function changePassword(newPassword) { + let accountDb = getAccountDb(); + + if (!isValidPassword(newPassword)) { + return { error: 'invalid-password' }; + } + + let hashed = hashPassword(newPassword); + accountDb.mutate("UPDATE auth SET extra_data = ? WHERE method = 'password'", [ + hashed, + ]); + return {}; +} diff --git a/packages/sync-server/src/app-account.js b/packages/sync-server/src/app-account.js new file mode 100644 index 00000000000..d1867d2a490 --- /dev/null +++ b/packages/sync-server/src/app-account.js @@ -0,0 +1,147 @@ +import express from 'express'; +import { + errorMiddleware, + requestLoggerMiddleware, +} from './util/middlewares.js'; +import validateSession, { validateAuthHeader } from './util/validate-user.js'; +import { + bootstrap, + needsBootstrap, + getLoginMethod, + listLoginMethods, + getUserInfo, + getActiveLoginMethod, +} from './account-db.js'; +import { changePassword, loginWithPassword } from './accounts/password.js'; +import { isValidRedirectUrl, loginWithOpenIdSetup } from './accounts/openid.js'; + +let app = express(); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(errorMiddleware); +app.use(requestLoggerMiddleware); +export { app as handlers }; + +// Non-authenticated endpoints: +// +// /needs-bootstrap +// /boostrap (special endpoint for setting up the instance, cant call again) +// /login + +app.get('/needs-bootstrap', (req, res) => { + res.send({ + status: 'ok', + data: { + bootstrapped: !needsBootstrap(), + loginMethod: getLoginMethod(), + availableLoginMethods: listLoginMethods(), + multiuser: getActiveLoginMethod() === 'openid', + }, + }); +}); + +app.post('/bootstrap', async (req, res) => { + let boot = await bootstrap(req.body); + + if (boot?.error) { + res.status(400).send({ status: 'error', reason: boot?.error }); + return; + } + res.send({ status: 'ok', data: boot }); +}); + +app.get('/login-methods', (req, res) => { + let methods = listLoginMethods(); + res.send({ status: 'ok', methods }); +}); + +app.post('/login', async (req, res) => { + let loginMethod = getLoginMethod(req); + console.log('Logging in via ' + loginMethod); + let tokenRes = null; + switch (loginMethod) { + case 'header': { + let headerVal = req.get('x-actual-password') || ''; + const obfuscated = + '*'.repeat(headerVal.length) || 'No password provided.'; + console.debug('HEADER VALUE: ' + obfuscated); + if (headerVal == '') { + res.send({ status: 'error', reason: 'invalid-header' }); + return; + } else { + if (validateAuthHeader(req)) { + tokenRes = loginWithPassword(headerVal); + } else { + res.send({ status: 'error', reason: 'proxy-not-trusted' }); + return; + } + } + break; + } + case 'openid': { + if (!isValidRedirectUrl(req.body.return_url)) { + res + .status(400) + .send({ status: 'error', reason: 'Invalid redirect URL' }); + return; + } + + let { error, url } = await loginWithOpenIdSetup(req.body.return_url); + if (error) { + res.status(400).send({ status: 'error', reason: error }); + return; + } + res.send({ status: 'ok', data: { redirect_url: url } }); + return; + } + + default: + tokenRes = loginWithPassword(req.body.password); + break; + } + let { error, token } = tokenRes; + + if (error) { + res.status(400).send({ status: 'error', reason: error }); + return; + } + + res.send({ status: 'ok', data: { token } }); +}); + +app.post('/change-password', (req, res) => { + let session = validateSession(req, res); + if (!session) return; + + let { error } = changePassword(req.body.password); + + if (error) { + res.status(400).send({ status: 'error', reason: error }); + return; + } + + res.send({ status: 'ok', data: {} }); +}); + +app.get('/validate', (req, res) => { + let session = validateSession(req, res); + if (session) { + const user = getUserInfo(session.user_id); + if (!user) { + res.status(400).send({ status: 'error', reason: 'User not found' }); + return; + } + + res.send({ + status: 'ok', + data: { + validated: true, + userName: user?.user_name, + permission: user?.role, + userId: session?.user_id, + displayName: user?.display_name, + loginMethod: session?.auth_method, + }, + }); + } +}); diff --git a/packages/sync-server/src/app-admin.js b/packages/sync-server/src/app-admin.js new file mode 100644 index 00000000000..f920a266be3 --- /dev/null +++ b/packages/sync-server/src/app-admin.js @@ -0,0 +1,409 @@ +import express from 'express'; +import * as uuid from 'uuid'; +import { + errorMiddleware, + requestLoggerMiddleware, + validateSessionMiddleware, +} from './util/middlewares.js'; +import validateSession from './util/validate-user.js'; +import { isAdmin } from './account-db.js'; +import * as UserService from './services/user-service.js'; + +let app = express(); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(requestLoggerMiddleware); + +export { app as handlers }; + +app.get('/owner-created/', (req, res) => { + try { + const ownerCount = UserService.getOwnerCount(); + res.json(ownerCount > 0); + } catch (error) { + res.status(500).json({ error: 'Failed to retrieve owner count' }); + } +}); + +app.get('/users/', validateSessionMiddleware, (req, res) => { + const users = UserService.getAllUsers(); + res.json( + users.map((u) => ({ + ...u, + owner: u.owner === 1, + enabled: u.enabled === 1, + })), + ); +}); + +app.post('/users', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(res.locals.user_id)) { + res.status(403).send({ + status: 'error', + reason: 'forbidden', + details: 'permission-not-found', + }); + return; + } + + const { userName, role, displayName, enabled } = req.body; + + if (!userName || !role) { + res.status(400).send({ + status: 'error', + reason: `${!userName ? 'user-cant-be-empty' : 'role-cant-be-empty'}`, + details: `${!userName ? 'Username' : 'Role'} cannot be empty`, + }); + return; + } + + const roleIdFromDb = UserService.validateRole(role); + if (!roleIdFromDb) { + res.status(400).send({ + status: 'error', + reason: 'role-does-not-exists', + details: 'Selected role does not exist', + }); + return; + } + + const userIdInDb = UserService.getUserByUsername(userName); + if (userIdInDb) { + res.status(400).send({ + status: 'error', + reason: 'user-already-exists', + details: `User ${userName} already exists`, + }); + return; + } + + const userId = uuid.v4(); + UserService.insertUser( + userId, + userName, + displayName || null, + enabled ? 1 : 0, + ); + + res.status(200).send({ status: 'ok', data: { id: userId } }); +}); + +app.patch('/users', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(res.locals.user_id)) { + res.status(403).send({ + status: 'error', + reason: 'forbidden', + details: 'permission-not-found', + }); + return; + } + + const { id, userName, role, displayName, enabled } = req.body; + + if (!userName || !role) { + res.status(400).send({ + status: 'error', + reason: `${!userName ? 'user-cant-be-empty' : 'role-cant-be-empty'}`, + details: `${!userName ? 'Username' : 'Role'} cannot be empty`, + }); + return; + } + + const roleIdFromDb = UserService.validateRole(role); + if (!roleIdFromDb) { + res.status(400).send({ + status: 'error', + reason: 'role-does-not-exists', + details: 'Selected role does not exist', + }); + return; + } + + const userIdInDb = UserService.getUserById(id); + if (!userIdInDb) { + res.status(400).send({ + status: 'error', + reason: 'cannot-find-user-to-update', + details: `Cannot find user ${userName} to update`, + }); + return; + } + + UserService.updateUserWithRole( + userIdInDb, + userName, + displayName || null, + enabled ? 1 : 0, + role, + ); + + res.status(200).send({ status: 'ok', data: { id: userIdInDb } }); +}); + +app.delete('/users', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(res.locals.user_id)) { + res.status(403).send({ + status: 'error', + reason: 'forbidden', + details: 'permission-not-found', + }); + return; + } + + const ids = req.body.ids; + let totalDeleted = 0; + ids.forEach((item) => { + const ownerId = UserService.getOwnerId(); + + if (item === ownerId) return; + + UserService.deleteUserAccess(item); + UserService.transferAllFilesFromUser(ownerId, item); + const usersDeleted = UserService.deleteUser(item); + totalDeleted += usersDeleted; + }); + + if (ids.length === totalDeleted) { + res + .status(200) + .send({ status: 'ok', data: { someDeletionsFailed: false } }); + } else { + res.status(400).send({ + status: 'error', + reason: 'not-all-deleted', + details: '', + }); + } +}); + +app.get('/access', validateSessionMiddleware, (req, res) => { + const fileId = req.query.fileId; + + const { granted } = UserService.checkFilePermission( + fileId, + res.locals.user_id, + ) || { + granted: 0, + }; + + if (granted === 0 && !isAdmin(res.locals.user_id)) { + res.status(403).send({ + status: 'error', + reason: 'forbidden', + details: 'permission-not-found', + }); + return false; + } + + const fileIdInDb = UserService.getFileById(fileId); + if (!fileIdInDb) { + res.status(404).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); + return false; + } + + const accesses = UserService.getUserAccess( + fileId, + res.locals.user_id, + isAdmin(res.locals.user_id), + ); + + res.json(accesses); +}); + +app.post('/access', (req, res) => { + const userAccess = req.body || {}; + const session = validateSession(req, res); + + if (!session) return; + + const { granted } = UserService.checkFilePermission( + userAccess.fileId, + session.user_id, + ) || { + granted: 0, + }; + + if (granted === 0 && !isAdmin(session.user_id)) { + res.status(400).send({ + status: 'error', + reason: 'file-denied', + details: "You don't have permissions over this file", + }); + return; + } + + const fileIdInDb = UserService.getFileById(userAccess.fileId); + if (!fileIdInDb) { + res.status(404).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); + return; + } + + if (!userAccess.userId) { + res.status(400).send({ + status: 'error', + reason: 'user-cant-be-empty', + details: 'User cannot be empty', + }); + return; + } + + if (UserService.countUserAccess(userAccess.fileId, userAccess.userId) > 0) { + res.status(400).send({ + status: 'error', + reason: 'user-already-have-access', + details: 'User already have access', + }); + return; + } + + UserService.addUserAccess(userAccess.userId, userAccess.fileId); + + res.status(200).send({ status: 'ok', data: {} }); +}); + +app.delete('/access', (req, res) => { + const fileId = req.query.fileId; + const session = validateSession(req, res); + if (!session) return; + + const { granted } = UserService.checkFilePermission( + fileId, + session.user_id, + ) || { + granted: 0, + }; + + if (granted === 0 && !isAdmin(session.user_id)) { + res.status(400).send({ + status: 'error', + reason: 'file-denied', + details: "You don't have permissions over this file", + }); + return; + } + + const fileIdInDb = UserService.getFileById(fileId); + if (!fileIdInDb) { + res.status(404).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); + return; + } + + const ids = req.body.ids; + let totalDeleted = UserService.deleteUserAccessByFileId(ids, fileId); + + if (ids.length === totalDeleted) { + res + .status(200) + .send({ status: 'ok', data: { someDeletionsFailed: false } }); + } else { + res.status(400).send({ + status: 'error', + reason: 'not-all-deleted', + details: '', + }); + } +}); + +app.get('/access/users', validateSessionMiddleware, async (req, res) => { + const fileId = req.query.fileId; + + const { granted } = UserService.checkFilePermission( + fileId, + res.locals.user_id, + ) || { + granted: 0, + }; + + if (granted === 0 && !isAdmin(res.locals.user_id)) { + res.status(400).send({ + status: 'error', + reason: 'file-denied', + details: "You don't have permissions over this file", + }); + return; + } + + const fileIdInDb = UserService.getFileById(fileId); + if (!fileIdInDb) { + res.status(404).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); + return; + } + + const users = UserService.getAllUserAccess(fileId); + res.json(users); +}); + +app.post( + '/access/transfer-ownership/', + validateSessionMiddleware, + (req, res) => { + const newUserOwner = req.body || {}; + + const { granted } = UserService.checkFilePermission( + newUserOwner.fileId, + res.locals.user_id, + ) || { + granted: 0, + }; + + if (granted === 0 && !isAdmin(res.locals.user_id)) { + res.status(400).send({ + status: 'error', + reason: 'file-denied', + details: "You don't have permissions over this file", + }); + return; + } + + const fileIdInDb = UserService.getFileById(newUserOwner.fileId); + if (!fileIdInDb) { + res.status(404).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); + return; + } + + if (!newUserOwner.newUserId) { + res.status(400).send({ + status: 'error', + reason: 'user-cant-be-empty', + details: 'Username cannot be empty', + }); + return; + } + + const newUserIdFromDb = UserService.getUserById(newUserOwner.newUserId); + if (newUserIdFromDb === 0) { + res.status(400).send({ + status: 'error', + reason: 'new-user-not-found', + details: 'New user not found', + }); + return; + } + + UserService.updateFileOwner(newUserOwner.newUserId, newUserOwner.fileId); + + res.status(200).send({ status: 'ok', data: {} }); + }, +); + +app.use(errorMiddleware); diff --git a/packages/sync-server/src/app-admin.test.js b/packages/sync-server/src/app-admin.test.js new file mode 100644 index 00000000000..29f22ec250c --- /dev/null +++ b/packages/sync-server/src/app-admin.test.js @@ -0,0 +1,380 @@ +import request from 'supertest'; +import { handlers as app } from './app-admin.js'; +import getAccountDb from './account-db.js'; +import { v4 as uuidv4 } from 'uuid'; + +const ADMIN_ROLE = 'ADMIN'; +const BASIC_ROLE = 'BASIC'; + +// Create user helper function +const createUser = (userId, userName, role, owner = 0, enabled = 1) => { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, owner, role], + ); +}; + +const deleteUser = (userId) => { + getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [userId]); + getAccountDb().mutate('DELETE FROM users WHERE id = ?', [userId]); +}; + +const createSession = (userId, sessionToken) => { + getAccountDb().mutate( + 'INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)', + [sessionToken, userId, Date.now() + 1000 * 60 * 60], // Expire in 1 hour + ); +}; + +const generateSessionToken = () => `token-${uuidv4()}`; + +describe('/admin', () => { + describe('/owner-created', () => { + it('should return 200 and true if an owner user is created', async () => { + const sessionToken = generateSessionToken(); + const adminId = uuidv4(); + createUser(adminId, 'admin', ADMIN_ROLE, 1); + createSession(adminId, sessionToken); + + const res = await request(app) + .get('/owner-created') + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(200); + expect(res.body).toBe(true); + }); + }); + + describe('/users', () => { + describe('GET /users', () => { + let sessionUserId, testUserId, sessionToken; + + beforeEach(() => { + sessionUserId = uuidv4(); + testUserId = uuidv4(); + sessionToken = generateSessionToken(); + + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); + createUser(testUserId, 'testUser', ADMIN_ROLE); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + }); + + it('should return 200 and a list of users', async () => { + const res = await request(app) + .get('/users') + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(200); + expect(res.body.length).toBeGreaterThan(0); + }); + }); + + describe('POST /users', () => { + let sessionUserId, sessionToken; + let createdUserId; + let duplicateUserId; + + beforeEach(() => { + sessionUserId = uuidv4(); + sessionToken = generateSessionToken(); + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); + }); + + afterEach(() => { + deleteUser(sessionUserId); + if (createdUserId) { + deleteUser(createdUserId); + createdUserId = null; + } + + if (duplicateUserId) { + deleteUser(duplicateUserId); + duplicateUserId = null; + } + }); + + it('should return 200 and create a new user', async () => { + const newUser = { + userName: 'user1', + displayName: 'User One', + enabled: 1, + owner: 0, + role: BASIC_ROLE, + }; + + const res = await request(app) + .post('/users') + .send(newUser) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + expect(res.body.data).toHaveProperty('id'); + + createdUserId = res.body.data.id; + }); + + it('should return 400 if the user already exists', async () => { + const newUser = { + userName: 'user1', + displayName: 'User One', + enabled: 1, + owner: 0, + role: BASIC_ROLE, + }; + + let res = await request(app) + .post('/users') + .send(newUser) + .set('x-actual-token', sessionToken); + + duplicateUserId = res.body.data.id; + + res = await request(app) + .post('/users') + .send(newUser) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('user-already-exists'); + }); + }); + + describe('PATCH /users', () => { + let sessionUserId, testUserId, sessionToken; + + beforeEach(() => { + sessionUserId = uuidv4(); + testUserId = uuidv4(); + sessionToken = generateSessionToken(); + + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); + createUser(testUserId, 'testUser', ADMIN_ROLE); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + }); + + it('should return 200 and update an existing user', async () => { + const userToUpdate = { + id: testUserId, + userName: 'updatedUser', + displayName: 'Updated User', + enabled: true, + role: BASIC_ROLE, + }; + + const res = await request(app) + .patch('/users') + .send(userToUpdate) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + expect(res.body.data.id).toBe(testUserId); + }); + + it('should return 400 if the user does not exist', async () => { + const userToUpdate = { + id: 'non-existing-id', + userName: 'nonexistinguser', + displayName: 'Non-existing User', + enabled: true, + role: BASIC_ROLE, + }; + + const res = await request(app) + .patch('/users') + .send(userToUpdate) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('cannot-find-user-to-update'); + }); + }); + + describe('POST /users/delete-all', () => { + let sessionUserId, testUserId, sessionToken; + + beforeEach(() => { + sessionUserId = uuidv4(); + testUserId = uuidv4(); + sessionToken = generateSessionToken(); + + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); + createUser(testUserId, 'testUser', ADMIN_ROLE); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + }); + + it('should return 200 and delete all specified users', async () => { + const userToDelete = { + ids: [testUserId], + }; + + const res = await request(app) + .delete('/users') + .send(userToDelete) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + expect(res.body.data.someDeletionsFailed).toBe(false); + }); + + it('should return 400 if not all users are deleted', async () => { + const userToDelete = { + ids: ['non-existing-id'], + }; + + const res = await request(app) + .delete('/users') + .send(userToDelete) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('not-all-deleted'); + }); + }); + }); + + describe('/access', () => { + describe('POST /access', () => { + let sessionUserId, testUserId, fileId, sessionToken; + + beforeEach(() => { + sessionUserId = uuidv4(); + testUserId = uuidv4(); + fileId = uuidv4(); + sessionToken = generateSessionToken(); + + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); + createUser(testUserId, 'testUser', ADMIN_ROLE); + getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ + fileId, + sessionUserId, + ]); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); + }); + + it('should return 200 and grant access to a user', async () => { + const newUserAccess = { + fileId, + userId: testUserId, + }; + + const res = await request(app) + .post('/access') + .send(newUserAccess) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + }); + + it('should return 400 if the user already has access', async () => { + const newUserAccess = { + fileId, + userId: testUserId, + }; + + await request(app) + .post('/access') + .send(newUserAccess) + .set('x-actual-token', sessionToken); + + const res = await request(app) + .post('/access') + .send(newUserAccess) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('user-already-have-access'); + }); + }); + + describe('DELETE /access', () => { + let sessionUserId, testUserId, fileId, sessionToken; + + beforeEach(() => { + sessionUserId = uuidv4(); + testUserId = uuidv4(); + fileId = uuidv4(); + sessionToken = generateSessionToken(); + + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); + createUser(testUserId, 'testUser', ADMIN_ROLE); + getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ + fileId, + sessionUserId, + ]); + getAccountDb().mutate( + 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', + [testUserId, fileId], + ); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); + }); + + it('should return 200 and delete access for the specified user', async () => { + const deleteAccess = { + ids: [testUserId], + }; + + const res = await request(app) + .delete('/access') + .send(deleteAccess) + .query({ fileId }) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + expect(res.body.data.someDeletionsFailed).toBe(false); + }); + + it('should return 400 if not all access deletions are successful', async () => { + const deleteAccess = { + ids: ['non-existing-id'], + }; + + const res = await request(app) + .delete('/access') + .send(deleteAccess) + .query({ fileId }) + .set('x-actual-token', sessionToken); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('not-all-deleted'); + }); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/README.md b/packages/sync-server/src/app-gocardless/README.md new file mode 100644 index 00000000000..23f8300e966 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/README.md @@ -0,0 +1,164 @@ +# Integration new bank + +If the default bank integration does not work for you, you can integrate a new bank by following these steps. + +1. Find in [this google doc](https://docs.google.com/spreadsheets/d/1ogpzydzotOltbssrc3IQ8rhBLlIZbQgm5QCiiNJrkyA/edit#gid=489769432) what is the identifier of the bank which you want to integrate. + +2. Launch frontend and backend server. + +3. In the frontend, create a new linked account selecting the institution which you are interested in. + + This will trigger the process of fetching the data from the bank and will log the data in the backend. Use this data to fill the logic of the bank class. + +4. Create new a bank class based on `app-gocardless/banks/sandboxfinance-sfin0000.js`. + + Name of the file and class should be created based on the ID of the integrated institution, found in step 1. + +5. Fill the logic of `normalizeAccount`, `normalizeTransaction`, `sortTransactions`, and `calculateStartingBalance` functions. + You do not need to fill every function, only those which are necessary for the integration to work. + + You should do it based on the data which you found in the logs. + + Example logs which help you to fill: + + - `normalizeAccount` function: + + ```log + Available account properties for new institution integration { + account: '{ + "iban": "PL00000000000000000987654321", + "currency": "PLN", + "ownerName": "John Example", + "displayName": "Product name", + "product": "Daily account", + "usage": "PRIV", + "ownerAddressUnstructured": [ + "POL", + "UL. Example 1", + "00-000 Warsaw" + ], + "id": "XXXXXXXX-XXXX-XXXXX-XXXXXX-XXXXXXXXX", + "created": "2023-01-18T12:15:16.502446Z", + "last_accessed": null, + "institution_id": "MBANK_RETAIL_BREXPLPW", + "status": "READY", + "owner_name": "", + "institution": { + "id": "MBANK_RETAIL_BREXPLPW", + "name": "mBank Retail", + "bic": "BREXPLPW", + "transaction_total_days": "90", + "countries": [ + "PL" + ], + "logo": "https://cdn.nordigen.com/ais/MBANK_RETAIL_BREXCZPP.png", + "supported_payments": {}, + "supported_features": [ + "access_scopes", + "business_accounts", + "card_accounts", + "corporate_accounts", + "pending_transactions", + "private_accounts" + ] + } + }' + } + ``` + + - `sortTransactions` function: + + ```log + Available (first 10) transactions properties for new integration of institution in sortTransactions function + { + top10SortedTransactions: '[ + { + "transactionId": "20220101001", + "bookingDate": "2022-01-01", + "valueDate": "2022-01-01", + "transactionAmount": { + "amount": "5.01", + "currency": "EUR" + }, + "creditorName": "JOHN EXAMPLE", + "creditorAccount": { + "iban": "PL00000000000000000987654321" + }, + "debtorName": "CHRIS EXAMPLE", + "debtorAccount": { + "iban": "PL12345000000000000987654321" + }, + "remittanceInformationUnstructured": "TEST BANK TRANSFER", + "remittanceInformationUnstructuredArray": [ + "TEST BANK TRANSFER" + ], + "balanceAfterTransaction": { + "balanceAmount": { + "amount": "448.52", + "currency": "EUR" + }, + "balanceType": "interimBooked" + }, + "internalTransactionId": "casfib7720c2a02c0331cw2" + } + ]' + } + ``` + + - `calculateStartingBalance` function: + + ```log + Available (first 10) transactions properties for new integration of institution in calculateStartingBalance function { + balances: '[ + { + "balanceAmount": { + "amount": "448.52", + "currency": "EUR" + }, + "balanceType": "forwardAvailable" + }, + { + "balanceAmount": { + "amount": "448.52", + "currency": "EUR" + }, + "balanceType": "interimBooked" + } + ]', + top10SortedTransactions: '[ + { + "transactionId": "20220101001", + "bookingDate": "2022-01-01", + "valueDate": "2022-01-01", + "transactionAmount": { + "amount": "5.01", + "currency": "EUR" + }, + "creditorName": "JOHN EXAMPLE", + "creditorAccount": { + "iban": "PL00000000000000000987654321" + }, + "debtorName": "CHRIS EXAMPLE", + "debtorAccount": { + "iban": "PL12345000000000000987654321" + }, + "remittanceInformationUnstructured": "TEST BANK TRANSFER", + "remittanceInformationUnstructuredArray": [ + "TEST BANK TRANSFER" + ], + "balanceAfterTransaction": { + "balanceAmount": { + "amount": "448.52", + "currency": "EUR" + }, + "balanceType": "interimBooked" + }, + "internalTransactionId": "casfib7720c2a02c0331cw2" + } + ]' + } + ``` + +6. Add new bank integration to `BankFactory` class in file `actual-server/app-gocardless/bank-factory.js` + +7. Remember to add tests for new bank integration in diff --git a/packages/sync-server/src/app-gocardless/app-gocardless.js b/packages/sync-server/src/app-gocardless/app-gocardless.js new file mode 100644 index 00000000000..b076a91b5bb --- /dev/null +++ b/packages/sync-server/src/app-gocardless/app-gocardless.js @@ -0,0 +1,272 @@ +import { isAxiosError } from 'axios'; +import express from 'express'; +import path from 'path'; +import { inspect } from 'util'; + +import { goCardlessService } from './services/gocardless-service.js'; +import { + AccountNotLinkedToRequisition, + GenericGoCardlessError, + RateLimitError, + RequisitionNotLinked, +} from './errors.js'; +import { handleError } from './util/handle-error.js'; +import { sha256String } from '../util/hash.js'; +import { + requestLoggerMiddleware, + validateSessionMiddleware, +} from '../util/middlewares.js'; + +const app = express(); +app.use(requestLoggerMiddleware); + +app.get('/link', function (req, res) { + res.sendFile('link.html', { root: path.resolve('./src/app-gocardless') }); +}); + +export { app as handlers }; +app.use(express.json()); +app.use(validateSessionMiddleware); + +app.post('/status', async (req, res) => { + res.send({ + status: 'ok', + data: { + configured: goCardlessService.isConfigured(), + }, + }); +}); + +app.post( + '/create-web-token', + handleError(async (req, res) => { + const { institutionId } = req.body; + const { origin } = req.headers; + + const { link, requisitionId } = await goCardlessService.createRequisition({ + institutionId, + host: origin, + }); + + res.send({ + status: 'ok', + data: { + link, + requisitionId, + }, + }); + }), +); + +app.post( + '/get-accounts', + handleError(async (req, res) => { + const { requisitionId } = req.body; + + try { + const { requisition, accounts } = + await goCardlessService.getRequisitionWithAccounts(requisitionId); + + res.send({ + status: 'ok', + data: { + ...requisition, + accounts: await Promise.all( + accounts.map(async (account) => + account?.iban + ? { ...account, iban: await sha256String(account.iban) } + : account, + ), + ), + }, + }); + } catch (error) { + if (error instanceof RequisitionNotLinked) { + res.send({ + status: 'ok', + requisitionStatus: error.details.requisitionStatus, + }); + } else { + throw error; + } + } + }), +); + +app.post( + '/get-banks', + handleError(async (req, res) => { + let { country, showDemo = false } = req.body; + + await goCardlessService.setToken(); + const data = await goCardlessService.getInstitutions(country); + + res.send({ + status: 'ok', + data: showDemo + ? [ + { + id: 'SANDBOXFINANCE_SFIN0000', + name: 'DEMO bank (used for testing bank-sync)', + }, + ...data, + ] + : data, + }); + }), +); + +app.post( + '/remove-account', + handleError(async (req, res) => { + let { requisitionId } = req.body; + + const data = await goCardlessService.deleteRequisition(requisitionId); + if (data.summary === 'Requisition deleted') { + res.send({ + status: 'ok', + data, + }); + } else { + res.send({ + status: 'error', + data: { + data, + reason: 'Can not delete requisition', + }, + }); + } + }), +); + +app.post( + '/transactions', + handleError(async (req, res) => { + const { + requisitionId, + startDate, + endDate, + accountId, + includeBalance = true, + } = req.body; + + try { + if (includeBalance) { + const { + balances, + institutionId, + startingBalance, + transactions: { booked, pending, all }, + } = await goCardlessService.getTransactionsWithBalance( + requisitionId, + accountId, + startDate, + endDate, + ); + + res.send({ + status: 'ok', + data: { + balances, + institutionId, + startingBalance, + transactions: { + booked, + pending, + all, + }, + }, + }); + } else { + const { + institutionId, + transactions: { booked, pending, all }, + } = await goCardlessService.getNormalizedTransactions( + requisitionId, + accountId, + startDate, + endDate, + ); + + res.send({ + status: 'ok', + data: { + institutionId, + transactions: { + booked, + pending, + all, + }, + }, + }); + } + } catch (error) { + const headers = error.details?.response?.headers ?? {}; + + const rateLimitHeaders = Object.fromEntries( + Object.entries(headers).filter(([key]) => + key.startsWith('http_x_ratelimit'), + ), + ); + + const sendErrorResponse = (data) => + res.send({ + status: 'ok', + data: { ...data, details: error.details, rateLimitHeaders }, + }); + + switch (true) { + case error instanceof RequisitionNotLinked: + sendErrorResponse({ + error_type: 'ITEM_ERROR', + error_code: 'ITEM_LOGIN_REQUIRED', + status: 'expired', + reason: + 'Access to account has expired as set in End User Agreement', + }); + break; + case error instanceof AccountNotLinkedToRequisition: + sendErrorResponse({ + error_type: 'INVALID_INPUT', + error_code: 'INVALID_ACCESS_TOKEN', + status: 'rejected', + reason: 'Account not linked with this requisition', + }); + break; + case error instanceof RateLimitError: + sendErrorResponse({ + error_type: 'RATE_LIMIT_EXCEEDED', + error_code: 'NORDIGEN_ERROR', + status: 'rejected', + reason: 'Rate limit exceeded', + }); + break; + case error instanceof GenericGoCardlessError: + console.log('Something went wrong', inspect(error, { depth: null })); + sendErrorResponse({ + error_type: 'SYNC_ERROR', + error_code: 'NORDIGEN_ERROR', + }); + break; + case isAxiosError(error): + console.log( + 'Something went wrong', + inspect(error.response?.data || error, { depth: null }), + ); + sendErrorResponse({ + error_type: 'SYNC_ERROR', + error_code: 'NORDIGEN_ERROR', + }); + break; + default: + console.log('Something went wrong', inspect(error, { depth: null })); + sendErrorResponse({ + error_type: 'UNKNOWN', + error_code: 'UNKNOWN', + reason: 'Something went wrong', + }); + break; + } + } + }), +); diff --git a/packages/sync-server/src/app-gocardless/bank-factory.js b/packages/sync-server/src/app-gocardless/bank-factory.js new file mode 100644 index 00000000000..33e83a78db3 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/bank-factory.js @@ -0,0 +1,132 @@ +import AbancaCaglesmm from './banks/abanca_caglesmm.js'; +import AbnamroAbnanl2a from './banks/abnamro_abnanl2a.js'; +import AmericanExpressAesudef1 from './banks/american_express_aesudef1.js'; +import BancsabadellBsabesbb from './banks/bancsabadell_bsabesbbb.js'; +import BankinterBkbkesmm from './banks/bankinter_bkbkesmm.js'; +import BankOfIrelandB365Bofiie2d from './banks/bank_of_ireland_b365_bofiie2d.js'; +import BelfiusGkccbebb from './banks/belfius_gkccbebb.js'; +import BerlinerSparkasseBeladebexxx from './banks/berliner_sparkasse_beladebexxx.js'; +import BnpBeGebabebb from './banks/bnp_be_gebabebb.js'; +import CbcCregbebb from './banks/cbc_cregbebb.js'; +import DanskebankDabno22 from './banks/danskebank_dabno22.js'; +import EasybankBawaatww from './banks/easybank_bawaatww.js'; +import EntercardSwednokk from './banks/entercard_swednokk.js'; +import FortuneoFtnofrp1xxx from './banks/fortuneo_ftnofrp1xxx.js'; +import HanseaticHstbdehh from './banks/hanseatic_hstbdehh.js'; +import HypeHyeeit22 from './banks/hype_hyeeit22.js'; +import IngIngbrobu from './banks/ing_ingbrobu.js'; +import IngIngddeff from './banks/ing_ingddeff.js'; +import IngPlIngbplpw from './banks/ing_pl_ingbplpw.js'; +import IntegrationBank from './banks/integration-bank.js'; +import IsyBankItbbitmm from './banks/isybank_itbbitmm.js'; +import KbcKredbebb from './banks/kbc_kredbebb.js'; +import MbankRetailBrexplpw from './banks/mbank_retail_brexplpw.js'; +import NationwideNaiagb21 from './banks/nationwide_naiagb21.js'; +import NbgEthngraaxxx from './banks/nbg_ethngraaxxx.js'; +import NorwegianXxNorwnok1 from './banks/norwegian_xx_norwnok1.js'; +import RevolutRevolt21 from './banks/revolut_revolt21.js'; +import SebKortBankAb from './banks/seb_kort_bank_ab.js'; +import SebPrivat from './banks/seb_privat.js'; +import SandboxfinanceSfin0000 from './banks/sandboxfinance_sfin0000.js'; +import SparnordSpnodk22 from './banks/sparnord_spnodk22.js'; +import SpkKarlsruheKarsde66 from './banks/spk_karlsruhe_karsde66.js'; +import SpkMarburgBiedenkopfHeladef1mar from './banks/spk_marburg_biedenkopf_heladef1mar.js'; +import SpkWormsAlzeyRiedMalade51wor from './banks/spk_worms_alzey_ried_malade51wor.js'; +import SskDusseldorfDussdeddxxx from './banks/ssk_dusseldorf_dussdeddxxx.js'; +import SwedbankHabalv22 from './banks/swedbank_habalv22.js'; +import VirginNrnbgb22 from './banks/virgin_nrnbgb22.js'; + +export const banks = [ + AbancaCaglesmm, + AbnamroAbnanl2a, + AmericanExpressAesudef1, + BancsabadellBsabesbb, + BankinterBkbkesmm, + BankOfIrelandB365Bofiie2d, + BelfiusGkccbebb, + BerlinerSparkasseBeladebexxx, + BnpBeGebabebb, + CbcCregbebb, + DanskebankDabno22, + EasybankBawaatww, + EntercardSwednokk, + FortuneoFtnofrp1xxx, + HanseaticHstbdehh, + HypeHyeeit22, + IngIngbrobu, + IngIngddeff, + IngPlIngbplpw, + IsyBankItbbitmm, + KbcKredbebb, + MbankRetailBrexplpw, + NationwideNaiagb21, + NbgEthngraaxxx, + NorwegianXxNorwnok1, + RevolutRevolt21, + SebKortBankAb, + SebPrivat, + SandboxfinanceSfin0000, + SparnordSpnodk22, + SpkKarlsruheKarsde66, + SpkMarburgBiedenkopfHeladef1mar, + SpkWormsAlzeyRiedMalade51wor, + SskDusseldorfDussdeddxxx, + SwedbankHabalv22, + VirginNrnbgb22, +]; + +export default (institutionId) => + banks.find((b) => b.institutionIds.includes(institutionId)) || + IntegrationBank; + +export const BANKS_WITH_LIMITED_HISTORY = [ + 'BANCA_AIDEXA_AIDXITMM', + 'BANCA_PATRIMONI_SENVITT1', + 'BANCA_SELLA_SELBIT2B', + 'BANKINTER_BKBKESMM', + 'BBVA_BBVAESMM', + 'BRED_BREDFRPPXXX', + 'CAIXABANK_CAIXESBB', + 'CARTALIS_CIMTITR1', + 'CESKA_SPORITELNA_LONG_GIBACZPX', + 'COOP_EKRDEE22', + 'DKB_BYLADEM1', + 'DOTS_HYEEIT22', + 'FINECO_FEBIITM2XXX', + 'FINECO_UK_FEBIITM2XXX', + 'FORTUNEO_FTNOFRP1XXX', + 'HYPE_BUSINESS_HYEEIT22', + 'HYPE_HYEEIT22', + 'ILLIMITY_ITTPIT2M', + 'INDUSTRA_MULTLV2X', + 'JEKYLL_JEYKLL002', + 'LABORALKUTXA_CLPEES2M', + 'LHV_LHVBEE22', + 'LUMINOR_AGBLLT2X', + 'LUMINOR_NDEAEE2X', + 'LUMINOR_NDEALT2X', + 'LUMINOR_NDEALV2X', + 'LUMINOR_RIKOEE22', + 'LUMINOR_RIKOLV2X', + 'MEDICINOSBANK_MDBALT22XXX', + 'NORDEA_NDEADKKK', + 'N26_NTSBDEB1', + 'OPYN_BITAITRRB2B', + 'PAYTIPPER_PAYTITM1', + 'REVOLUT_REVOLT21', + 'SANTANDER_BSCHESMM', + 'SANTANDER_DE_SCFBDE33', + 'SEB_CBVILT2X', + 'SEB_EEUHEE2X', + 'SEB_UNLALV2X', + 'SELLA_PERSONAL_CREDIT_SELBIT22', + 'BANCOACTIVOBANK_ACTVPTPL', + 'SMARTIKA_SELBIT22', + 'SWEDBANK_HABAEE2X', + 'SWEDBANK_HABALT22', + 'SWEDBANK_HABALV22', + 'SWEDBANK_SWEDSESS', + 'TIM_HYEEIT22', + 'TOT_SELBIT2B', + 'VUB_BANKA_SUBASKBX', +]; diff --git a/packages/sync-server/src/app-gocardless/banks/1822-direkt-heladef1822.js b/packages/sync-server/src/app-gocardless/banks/1822-direkt-heladef1822.js new file mode 100644 index 00000000000..74a2393547b --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/1822-direkt-heladef1822.js @@ -0,0 +1,16 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['DIREKT_HELADEF1822'], + + normalizeTransaction(transaction, booked) { + transaction.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured ?? + transaction.remittanceInformationStructured; + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/1822_direkt_heladef1822.js b/packages/sync-server/src/app-gocardless/banks/1822_direkt_heladef1822.js new file mode 100644 index 00000000000..74a2393547b --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/1822_direkt_heladef1822.js @@ -0,0 +1,16 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['DIREKT_HELADEF1822'], + + normalizeTransaction(transaction, booked) { + transaction.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured ?? + transaction.remittanceInformationStructured; + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/FORTUNEO_FTNOFRP1XXX.js b/packages/sync-server/src/app-gocardless/banks/FORTUNEO_FTNOFRP1XXX.js new file mode 100644 index 00000000000..e05634e4e91 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/FORTUNEO_FTNOFRP1XXX.js @@ -0,0 +1,62 @@ +import { formatPayeeName } from '../../util/payee-name.js'; +import Fallback from './integration-bank.js'; +import * as d from 'date-fns'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['FORTUNEO_FTNOFRP1XXX'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + // Most of the information from the transaction is in the remittanceInformationUnstructuredArray field. + // We extract the creditor and debtor names from this field. + // The remittanceInformationUnstructuredArray field usually contain keywords like "Vir" for + // bank transfers or "Carte 03/06" for card payments, as well as the date. + // We remove these keywords to get a cleaner payee name. + const keywordsToRemove = [ + 'VIR INST', + 'VIR', + 'PRLV', + 'ANN CARTE', + 'CARTE \\d{2}\\/\\d{2}', + ]; + + const details = + transaction.remittanceInformationUnstructuredArray.join(' '); + const amount = transaction.transactionAmount.amount; + + const regex = new RegExp(keywordsToRemove.join('|'), 'g'); + const payeeName = details.replace(regex, '').trim(); + + // The amount is negative for outgoing transactions, positive for incoming transactions. + const isCreditorPayee = parseFloat(amount) < 0; + + // The payee name is the creditor name for outgoing transactions and the debtor name for incoming transactions. + const creditorName = isCreditorPayee ? payeeName : null; + const debtorName = isCreditorPayee ? null : payeeName; + + transaction.creditorName = creditorName; + transaction.debtorName = debtorName; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/HANSEATIC_HSTBDEHH.js b/packages/sync-server/src/app-gocardless/banks/HANSEATIC_HSTBDEHH.js new file mode 100644 index 00000000000..143f92bf496 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/HANSEATIC_HSTBDEHH.js @@ -0,0 +1,10 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['HANSEATIC_HSTBDEHH'], + + accessValidForDays: 89, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/abanca-caglesmm.js b/packages/sync-server/src/app-gocardless/banks/abanca-caglesmm.js new file mode 100644 index 00000000000..bd485331bbb --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/abanca-caglesmm.js @@ -0,0 +1,24 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ABANCA_CAGLESMM', 'ABANCA_CAGLPTPL'], + + accessValidForDays: 180, + + // Abanca transactions doesn't get the creditorName/debtorName properly + normalizeTransaction(transaction, _booked) { + transaction.creditorName = transaction.remittanceInformationStructured; + transaction.debtorName = transaction.remittanceInformationStructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js b/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js new file mode 100644 index 00000000000..bd485331bbb --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js @@ -0,0 +1,24 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ABANCA_CAGLESMM', 'ABANCA_CAGLPTPL'], + + accessValidForDays: 180, + + // Abanca transactions doesn't get the creditorName/debtorName properly + normalizeTransaction(transaction, _booked) { + transaction.creditorName = transaction.remittanceInformationStructured; + transaction.debtorName = transaction.remittanceInformationStructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js b/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js new file mode 100644 index 00000000000..0bf6e216f75 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js @@ -0,0 +1,59 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ABNAMRO_ABNANL2A'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + // There is no remittanceInformationUnstructured, so we'll make it + transaction.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructuredArray.join(', '); + + // Remove clutter to extract the payee from remittanceInformationUnstructured ... + // ... when not otherwise provided. + const payeeName = transaction.remittanceInformationUnstructuredArray + .map((el) => el.match(/^(?:.*\*)?(.+),PAS\d+$/)) + .find((match) => match)?.[1]; + transaction.debtorName = transaction.debtorName || payeeName; + transaction.creditorName = transaction.creditorName || payeeName; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDateTime.slice(0, 10), + }; + }, + + sortTransactions(transactions = []) { + return transactions.sort( + (a, b) => +new Date(b.valueDateTime) - +new Date(a.valueDateTime), + ); + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + if (sortedTransactions.length) { + const oldestTransaction = + sortedTransactions[sortedTransactions.length - 1]; + const oldestKnownBalance = amountToInteger( + oldestTransaction.balanceAfterTransaction.balanceAmount.amount, + ); + const oldestTransactionAmount = amountToInteger( + oldestTransaction.transactionAmount.amount, + ); + + return oldestKnownBalance - oldestTransactionAmount; + } else { + return amountToInteger( + balances.find((balance) => 'interimBooked' === balance.balanceType) + .balanceAmount.amount, + ); + } + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/american-express-aesudef1.js b/packages/sync-server/src/app-gocardless/banks/american-express-aesudef1.js new file mode 100644 index 00000000000..1f5e22833be --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/american-express-aesudef1.js @@ -0,0 +1,55 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['AMERICAN_EXPRESS_AESUDEF1'], + + accessValidForDays: 180, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + // The `iban` field for these American Express cards is actually a masked + // version of the PAN. No IBAN is provided. + mask: account.iban.slice(-5), + iban: null, + name: [account.details, `(${account.iban.slice(-5)})`].join(' '), + official_name: account.details, + // The Actual account `type` field is legacy and is currently not used + // for anything, so we leave it as the default of `checking`. + type: 'checking', + }; + }, + + normalizeTransaction(transaction, _booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate, + }; + }, + + /** + * For AMERICAN_EXPRESS_AESUDEF1 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use the non-standard `information` balance type + * which is the only one provided for American Express. + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'information' === balance.balanceType.toString(), + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js b/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js new file mode 100644 index 00000000000..2ae8168dfc9 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js @@ -0,0 +1,51 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['AMERICAN_EXPRESS_AESUDEF1'], + + accessValidForDays: 180, + + normalizeAccount(account) { + return { + ...Fallback.normalizeAccount(account), + // The `iban` field for these American Express cards is actually a masked + // version of the PAN. No IBAN is provided. + mask: account.iban.slice(-5), + iban: null, + name: [account.details, `(${account.iban.slice(-5)})`].join(' '), + official_name: account.details, + }; + }, + + normalizeTransaction(transaction, _booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate, + }; + }, + + /** + * For AMERICAN_EXPRESS_AESUDEF1 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use the non-standard `information` balance type + * which is the only one provided for American Express. + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'information' === balance.balanceType.toString(), + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/bancsabadell-bsabesbbb.js b/packages/sync-server/src/app-gocardless/banks/bancsabadell-bsabesbbb.js new file mode 100644 index 00000000000..832fb8953cf --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/bancsabadell-bsabesbbb.js @@ -0,0 +1,37 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['BANCSABADELL_BSABESBB'], + + accessValidForDays: 180, + + // Sabadell transactions don't get the creditorName/debtorName properly + normalizeTransaction(transaction, _booked) { + const amount = transaction.transactionAmount.amount; + + // The amount is negative for outgoing transactions, positive for incoming transactions. + const isCreditorPayee = Number.parseFloat(amount) < 0; + + const payeeName = transaction.remittanceInformationUnstructuredArray + .join(' ') + .trim(); + + // The payee name is the creditor name for outgoing transactions and the debtor name for incoming transactions. + const creditorName = isCreditorPayee ? payeeName : null; + const debtorName = isCreditorPayee ? null : payeeName; + + transaction.creditorName = creditorName; + transaction.debtorName = debtorName; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js b/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js new file mode 100644 index 00000000000..832fb8953cf --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js @@ -0,0 +1,37 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['BANCSABADELL_BSABESBB'], + + accessValidForDays: 180, + + // Sabadell transactions don't get the creditorName/debtorName properly + normalizeTransaction(transaction, _booked) { + const amount = transaction.transactionAmount.amount; + + // The amount is negative for outgoing transactions, positive for incoming transactions. + const isCreditorPayee = Number.parseFloat(amount) < 0; + + const payeeName = transaction.remittanceInformationUnstructuredArray + .join(' ') + .trim(); + + // The payee name is the creditor name for outgoing transactions and the debtor name for incoming transactions. + const creditorName = isCreditorPayee ? payeeName : null; + const debtorName = isCreditorPayee ? null : payeeName; + + transaction.creditorName = creditorName; + transaction.debtorName = debtorName; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/bank.interface.ts b/packages/sync-server/src/app-gocardless/banks/bank.interface.ts new file mode 100644 index 00000000000..21e87581567 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/bank.interface.ts @@ -0,0 +1,44 @@ +import { + DetailedAccountWithInstitution, + NormalizedAccountDetails, +} from '../gocardless.types.js'; +import { Transaction, Balance } from '../gocardless-node.types.js'; + +export interface IBank { + institutionIds: string[]; + + accessValidForDays: number; + + /** + * Returns normalized object with required data for the frontend + */ + normalizeAccount: ( + account: DetailedAccountWithInstitution, + ) => NormalizedAccountDetails; + + /** + * Returns a normalized transaction object + * + * The GoCardless integrations with different banks are very inconsistent in + * what each of the different date fields actually mean, so this function is + * expected to set a `date` field which corresponds to the expected + * transaction date. + */ + normalizeTransaction: ( + transaction: Transaction, + booked: boolean, + ) => (Transaction & { date?: string; payeeName: string }) | null; + + /** + * Function sorts an array of transactions from newest to oldest + */ + sortTransactions: <T extends Transaction>(transactions: T[]) => T[]; + + /** + * Calculates account balance before which was before transactions provided in sortedTransactions param + */ + calculateStartingBalance: ( + sortedTransactions: Transaction[], + balances: Balance[], + ) => number; +} diff --git a/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js b/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js new file mode 100644 index 00000000000..6e677775d3b --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js @@ -0,0 +1,39 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['BANK_OF_IRELAND_B365_BOFIIE2D'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, booked) { + transaction.remittanceInformationUnstructured = fixupPayee( + transaction.remittanceInformationUnstructured, + ); + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; + +function fixupPayee(/** @type {string} */ payee) { + let fixedPayee = payee; + + // remove all duplicate whitespace + fixedPayee = fixedPayee.replace(/\s+/g, ' ').trim(); + + // remove date prefix + fixedPayee = fixedPayee.replace(/^(POS)?(C)?[0-9]{1,2}\w{3}/, '').trim(); + + // remove direct debit postfix + fixedPayee = fixedPayee.replace(/sepa dd$/i, '').trim(); + + // remove bank transfer prefix + fixedPayee = fixedPayee.replace(/^365 online/i, '').trim(); + + // remove curve card prefix + fixedPayee = fixedPayee.replace(/^CRV\*/, '').trim(); + + return fixedPayee; +} diff --git a/packages/sync-server/src/app-gocardless/banks/bankinter-bkbkesmm.js b/packages/sync-server/src/app-gocardless/banks/bankinter-bkbkesmm.js new file mode 100644 index 00000000000..c7e17059f4a --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/bankinter-bkbkesmm.js @@ -0,0 +1,43 @@ +import Fallback from './integration-bank.js'; + +import { printIban } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['BANKINTER_BKBKESMM'], + + accessValidForDays: 90, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: account.iban.slice(-4), + iban: account.iban, + name: [account.name, printIban(account)].join(' '), + official_name: account.product, + type: 'checking', + }; + }, + + normalizeTransaction(transaction, _booked) { + transaction.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured + .replaceAll(/\/Txt\/(\w\|)?/gi, '') + .replaceAll(';', ' '); + + transaction.debtorName = transaction.debtorName?.replaceAll(';', ' '); + transaction.creditorName = + transaction.creditorName?.replaceAll(';', ' ') ?? + transaction.remittanceInformationUnstructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js b/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js new file mode 100644 index 00000000000..92672d6defb --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js @@ -0,0 +1,30 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['BANKINTER_BKBKESMM'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + transaction.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured + .replaceAll(/\/Txt\/(\w\|)?/gi, '') + .replaceAll(';', ' '); + + transaction.debtorName = transaction.debtorName?.replaceAll(';', ' '); + transaction.creditorName = + transaction.creditorName?.replaceAll(';', ' ') ?? + transaction.remittanceInformationUnstructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js b/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js new file mode 100644 index 00000000000..ebec54e1269 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js @@ -0,0 +1,24 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['BELFIUS_GKCCBEBB'], + + accessValidForDays: 180, + + // The problem is that we have transaction with duplicated transaction ids. + // This is not expected and the nordigen api has a work-around for some backs + // They will set an internalTransactionId which is unique + normalizeTransaction(transaction, _booked) { + return { + ...transaction, + transactionId: transaction.internalTransactionId, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js b/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js new file mode 100644 index 00000000000..2ef27d7d635 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js @@ -0,0 +1,86 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['BERLINER_SPARKASSE_BELADEBEXXX'], + + accessValidForDays: 180, + + /** + * Following the GoCardless documentation[0] we should prefer `bookingDate` + * here, though some of their bank integrations uses the date field + * differently from what's described in their documentation and so it's + * sometimes necessary to use `valueDate` instead. + * + * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions + */ + normalizeTransaction(transaction, _booked) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + let remittanceInformationUnstructured; + + if (transaction.remittanceInformationUnstructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured; + } else if (transaction.remittanceInformationStructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructured; + } else if (transaction.remittanceInformationStructuredArray?.length > 0) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructuredArray?.join(' '); + } + + if (transaction.additionalInformation) + remittanceInformationUnstructured += + ' ' + transaction.additionalInformation; + + const usefulCreditorName = + transaction.ultimateCreditor || + transaction.creditorName || + transaction.debtorName; + + transaction.creditorName = usefulCreditorName; + transaction.remittanceInformationUnstructured = + remittanceInformationUnstructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + /** + * For SANDBOXFINANCE_SFIN0000 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `interimBooked` balance type because + * it includes transaction placed during current day + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimAvailable' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/bnp-be-gebabebb.js b/packages/sync-server/src/app-gocardless/banks/bnp-be-gebabebb.js new file mode 100644 index 00000000000..9af2fa674b9 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/bnp-be-gebabebb.js @@ -0,0 +1,79 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: [ + 'FINTRO_BE_GEBABEBB', + 'HELLO_BE_GEBABEBB', + 'BNP_BE_GEBABEBB', + ], + + accessValidForDays: 180, + + /** BNP_BE_GEBABEBB provides a lot of useful information via the 'additionalField' + * There does not seem to be a specification of this field, but the following information is contained in its subfields: + * - for pending transactions: the 'atmPosName' + * - for booked transactions: the 'narrative'. + * This narrative subfield is most useful as it contains information required to identify the transaction, + * especially in case of debit card or instant payment transactions. + * Do note that the narrative subfield ALSO contains the remittance information if any. + * The goal of the normalization is to place any relevant information of the additionalInformation + * field in the remittanceInformationUnstructuredArray field. + */ + normalizeTransaction(transaction, _booked) { + // Extract the creditor name to fill it in with information from the + // additionalInformation field in case it's not yet defined. + let creditorName = transaction.creditorName; + + if (transaction.additionalInformation) { + let additionalInformationObject = {}; + const additionalInfoRegex = /(, )?([^:]+): ((\[.*?\])|([^,]*))/g; + let matches = + transaction.additionalInformation.matchAll(additionalInfoRegex); + if (matches) { + let creditorNameFromNarrative; // Possible value for creditorName + for (let match of matches) { + let key = match[2].trim(); + let value = (match[4] || match[5]).trim(); + if (key === 'narrative') { + // Set narrativeName to the first element in the "narrative" array. + let first_value = value.matchAll(/'(.+?)'/g)?.next().value; + creditorNameFromNarrative = first_value + ? first_value[1].trim() + : undefined; + } + // Remove square brackets and single quotes and commas + value = value.replace(/[[\]',]/g, ''); + additionalInformationObject[key] = value; + } + // Keep existing unstructuredArray and add atmPosName and narrative + transaction.remittanceInformationUnstructuredArray = [ + transaction.remittanceInformationUnstructuredArray ?? '', + additionalInformationObject?.atmPosName ?? '', + additionalInformationObject?.narrative ?? '', + ].filter(Boolean); + + // If the creditor name doesn't exist in the original transactions, + // set it to the atmPosName or narrativeName if they exist; otherwise + // leave empty and let the default rules handle it. + creditorName = + creditorName ?? + additionalInformationObject?.atmPosName ?? + creditorNameFromNarrative ?? + null; + } + } + + transaction.creditorName = creditorName; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDate || transaction.bookingDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js b/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js new file mode 100644 index 00000000000..9af2fa674b9 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js @@ -0,0 +1,79 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: [ + 'FINTRO_BE_GEBABEBB', + 'HELLO_BE_GEBABEBB', + 'BNP_BE_GEBABEBB', + ], + + accessValidForDays: 180, + + /** BNP_BE_GEBABEBB provides a lot of useful information via the 'additionalField' + * There does not seem to be a specification of this field, but the following information is contained in its subfields: + * - for pending transactions: the 'atmPosName' + * - for booked transactions: the 'narrative'. + * This narrative subfield is most useful as it contains information required to identify the transaction, + * especially in case of debit card or instant payment transactions. + * Do note that the narrative subfield ALSO contains the remittance information if any. + * The goal of the normalization is to place any relevant information of the additionalInformation + * field in the remittanceInformationUnstructuredArray field. + */ + normalizeTransaction(transaction, _booked) { + // Extract the creditor name to fill it in with information from the + // additionalInformation field in case it's not yet defined. + let creditorName = transaction.creditorName; + + if (transaction.additionalInformation) { + let additionalInformationObject = {}; + const additionalInfoRegex = /(, )?([^:]+): ((\[.*?\])|([^,]*))/g; + let matches = + transaction.additionalInformation.matchAll(additionalInfoRegex); + if (matches) { + let creditorNameFromNarrative; // Possible value for creditorName + for (let match of matches) { + let key = match[2].trim(); + let value = (match[4] || match[5]).trim(); + if (key === 'narrative') { + // Set narrativeName to the first element in the "narrative" array. + let first_value = value.matchAll(/'(.+?)'/g)?.next().value; + creditorNameFromNarrative = first_value + ? first_value[1].trim() + : undefined; + } + // Remove square brackets and single quotes and commas + value = value.replace(/[[\]',]/g, ''); + additionalInformationObject[key] = value; + } + // Keep existing unstructuredArray and add atmPosName and narrative + transaction.remittanceInformationUnstructuredArray = [ + transaction.remittanceInformationUnstructuredArray ?? '', + additionalInformationObject?.atmPosName ?? '', + additionalInformationObject?.narrative ?? '', + ].filter(Boolean); + + // If the creditor name doesn't exist in the original transactions, + // set it to the atmPosName or narrativeName if they exist; otherwise + // leave empty and let the default rules handle it. + creditorName = + creditorName ?? + additionalInformationObject?.atmPosName ?? + creditorNameFromNarrative ?? + null; + } + } + + transaction.creditorName = creditorName; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDate || transaction.bookingDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js b/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js new file mode 100644 index 00000000000..94ab202859e --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js @@ -0,0 +1,38 @@ +import { extractPayeeNameFromRemittanceInfo } from './util/extract-payeeName-from-remittanceInfo.js'; +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['CBC_CREGBEBB'], + + accessValidForDays: 180, + + /** + * For negative amounts, the only payee information we have is returned in + * remittanceInformationUnstructured. + */ + normalizeTransaction(transaction, _booked) { + if (Number(transaction.transactionAmount.amount) > 0) { + return { + ...transaction, + payeeName: + transaction.debtorName || + transaction.remittanceInformationUnstructured, + date: transaction.bookingDate || transaction.valueDate, + }; + } + + return { + ...transaction, + payeeName: + transaction.creditorName || + extractPayeeNameFromRemittanceInfo( + transaction.remittanceInformationUnstructured, + ['Paiement', 'Domiciliation', 'Transfert', 'Ordre permanent'], + ), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/danskebank-dabno22.js b/packages/sync-server/src/app-gocardless/banks/danskebank-dabno22.js new file mode 100644 index 00000000000..99fa891aad1 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/danskebank-dabno22.js @@ -0,0 +1,60 @@ +import Fallback from './integration-bank.js'; + +import { printIban, amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['DANSKEBANK_DABANO22'], + + accessValidForDays: 180, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: account.iban.slice(-4), + iban: account.iban, + name: [account.name, printIban(account)].join(' '), + official_name: account.name, + type: 'checking', + }; + }, + + normalizeTransaction(transaction, _booked) { + /** + * Danske Bank appends the EndToEndID: NOTPROVIDED to + * remittanceInformationUnstructured, cluttering the data. + * + * We clean thais up by removing any instances of this string from all transactions. + * + */ + transaction.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured.replace( + '\nEndToEndID: NOTPROVIDED', + '', + ); + + /** + * The valueDate in transactions from Danske Bank is not the one expected, but rather the date + * the funds are expected to be paid back for credit accounts. + */ + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate, + }; + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => balance.balanceType === 'interimAvailable', + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js b/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js new file mode 100644 index 00000000000..3d83ea95067 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js @@ -0,0 +1,48 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['DANSKEBANK_DABANO22'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + /** + * Danske Bank appends the EndToEndID: NOTPROVIDED to + * remittanceInformationUnstructured, cluttering the data. + * + * We clean thais up by removing any instances of this string from all transactions. + * + */ + transaction.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured.replace( + '\nEndToEndID: NOTPROVIDED', + '', + ); + + /** + * The valueDate in transactions from Danske Bank is not the one expected, but rather the date + * the funds are expected to be paid back for credit accounts. + */ + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate, + }; + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => balance.balanceType === 'interimAvailable', + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/easybank-bawaatww.js b/packages/sync-server/src/app-gocardless/banks/easybank-bawaatww.js new file mode 100644 index 00000000000..f93ab951f4a --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/easybank-bawaatww.js @@ -0,0 +1,61 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; +import d from 'date-fns'; +import { title } from '../../util/title/index.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['EASYBANK_BAWAATWW'], + + accessValidForDays: 179, + + // If date is same, sort by transactionId + sortTransactions: (transactions = []) => + transactions.sort((a, b) => { + const diff = + +new Date(b.valueDate || b.bookingDate) - + +new Date(a.valueDate || a.bookingDate); + if (diff != 0) return diff; + return parseInt(b.transactionId) - parseInt(a.transactionId); + }), + + normalizeTransaction(transaction, _booked) { + const date = transaction.bookingDate || transaction.valueDate; + + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + let payeeName = formatPayeeName(transaction); + if (!payeeName) payeeName = extractPayeeName(transaction); + + return { + ...transaction, + payeeName: payeeName, + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + }, +}; + +/** + * Extracts the payee name from the remittanceInformationStructured + * @param {import('../gocardless-node.types.js').Transaction} transaction + */ +function extractPayeeName(transaction) { + const structured = transaction.remittanceInformationStructured; + // The payee name is betweeen the transaction timestamp (11.07. 11:36) and the location, that starts with \\ + const regex = /\d{2}\.\d{2}\. \d{2}:\d{2}(.*)\\\\/; + const matches = structured.match(regex); + if (matches && matches.length > 1 && matches[1]) { + return title(matches[1]); + } else { + // As a fallback if still no payee is found, the whole information is used + return structured; + } +} diff --git a/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js b/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js new file mode 100644 index 00000000000..f93ab951f4a --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js @@ -0,0 +1,61 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; +import d from 'date-fns'; +import { title } from '../../util/title/index.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['EASYBANK_BAWAATWW'], + + accessValidForDays: 179, + + // If date is same, sort by transactionId + sortTransactions: (transactions = []) => + transactions.sort((a, b) => { + const diff = + +new Date(b.valueDate || b.bookingDate) - + +new Date(a.valueDate || a.bookingDate); + if (diff != 0) return diff; + return parseInt(b.transactionId) - parseInt(a.transactionId); + }), + + normalizeTransaction(transaction, _booked) { + const date = transaction.bookingDate || transaction.valueDate; + + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + let payeeName = formatPayeeName(transaction); + if (!payeeName) payeeName = extractPayeeName(transaction); + + return { + ...transaction, + payeeName: payeeName, + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + }, +}; + +/** + * Extracts the payee name from the remittanceInformationStructured + * @param {import('../gocardless-node.types.js').Transaction} transaction + */ +function extractPayeeName(transaction) { + const structured = transaction.remittanceInformationStructured; + // The payee name is betweeen the transaction timestamp (11.07. 11:36) and the location, that starts with \\ + const regex = /\d{2}\.\d{2}\. \d{2}:\d{2}(.*)\\\\/; + const matches = structured.match(regex); + if (matches && matches.length > 1 && matches[1]) { + return title(matches[1]); + } else { + // As a fallback if still no payee is found, the whole information is used + return structured; + } +} diff --git a/packages/sync-server/src/app-gocardless/banks/entercard-swednokk.js b/packages/sync-server/src/app-gocardless/banks/entercard-swednokk.js new file mode 100644 index 00000000000..2742e83bfd3 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/entercard-swednokk.js @@ -0,0 +1,59 @@ +import * as d from 'date-fns'; +import { + amountToInteger, + printIban, + sortByBookingDateOrValueDate, +} from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + institutionIds: ['ENTERCARD_SWEDNOKK'], + + accessValidForDays: 180, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: (account?.iban || '0000').slice(-4), + iban: account?.iban || null, + name: [account.name, printIban(account), account.currency] + .filter(Boolean) + .join(' '), + official_name: `integration-${account.institution_id}`, + type: 'checking', + }; + }, + + normalizeTransaction(transaction, _booked) { + // GoCardless's Entercard integration returns forex transactions with the + // foreign amount in `transactionAmount`, but at least the amount actually + // billed to the account is now available in + // `remittanceInformationUnstructured`. + const remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured; + if (remittanceInformationUnstructured.startsWith('billingAmount: ')) { + transaction.transactionAmount = { + amount: remittanceInformationUnstructured.substring(15), + currency: 'SEK', + }; + } + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: d.format(d.parseISO(transaction.valueDate), 'yyyy-MM-dd'), + }; + }, + + sortTransactions(transactions = []) { + return sortByBookingDateOrValueDate(transactions); + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(balances[0]?.balanceAmount?.amount || 0)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js b/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js new file mode 100644 index 00000000000..daafbfb89c5 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js @@ -0,0 +1,42 @@ +import * as d from 'date-fns'; + +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ENTERCARD_SWEDNOKK'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + // GoCardless's Entercard integration returns forex transactions with the + // foreign amount in `transactionAmount`, but at least the amount actually + // billed to the account is now available in + // `remittanceInformationUnstructured`. + const remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured; + if (remittanceInformationUnstructured.startsWith('billingAmount: ')) { + transaction.transactionAmount = { + amount: remittanceInformationUnstructured.substring(15), + currency: 'SEK', + }; + } + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: d.format(d.parseISO(transaction.valueDate), 'yyyy-MM-dd'), + }; + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(balances[0]?.balanceAmount?.amount || 0)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js b/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js new file mode 100644 index 00000000000..d69efda8e74 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js @@ -0,0 +1,77 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['HYPE_HYEEIT22'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + /** Online card payments - identified by "crd" transaction code + * always start with PAGAMENTO PRESSO + <payee name> + */ + if (transaction.proprietaryBankTransactionCode == 'crd') { + // remove PAGAMENTO PRESSO and set payee name + transaction.debtorName = + transaction.remittanceInformationUnstructured?.slice( + 'PAGAMENTO PRESSO '.length, + ); + } + /** + * In-app money transfers (p2p) and bank transfers (bon) have remittance info structure like + * DENARO (INVIATO/RICEVUTO) (A/DA) {payee_name} - {payment_info} (p2p) + * HAI (INVIATO/RICEVUTO) UN BONIFICO (A/DA) {payee_name} - {payment_info} (bon) + */ + if ( + transaction.proprietaryBankTransactionCode == 'p2p' || + transaction.proprietaryBankTransactionCode == 'bon' + ) { + // keep only {payment_info} portion of remittance info + // NOTE: if {payee_name} contains dashes (unlikely / impossible?), this probably gets bugged! + let infoIdx = + transaction.remittanceInformationUnstructured.indexOf(' - ') + 3; + transaction.remittanceInformationUnstructured = + infoIdx == -1 + ? transaction.remittanceInformationUnstructured + : transaction.remittanceInformationUnstructured.slice(infoIdx).trim(); + } + /** + * CONVERT ESCAPED UNICODE TO CODEPOINTS + * p2p payments allow user to write arbitrary unicode strings as messages + * gocardless reports unicode codepoints as \Uxxxx + * so it groups them in 4bytes bundles + * the code below assumes this is always the case + */ + if (transaction.proprietaryBankTransactionCode == 'p2p') { + let str = transaction.remittanceInformationUnstructured; + let idx = str.indexOf('\\U'); + let start_idx = idx; + let codepoints = []; + while (idx !== -1) { + codepoints.push(parseInt(str.slice(idx + 2, idx + 6), 16)); + let next_idx = str.indexOf('\\U', idx + 6); + if (next_idx == idx + 6) { + idx = next_idx; + continue; + } + str = + str.slice(0, start_idx) + + String.fromCodePoint(...codepoints) + + str.slice(idx + 6); + codepoints = []; + idx = str.indexOf('\\U'); // slight inefficiency? + start_idx = idx; + } + transaction.remittanceInformationUnstructured = str; + } + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDate || transaction.bookingDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/ing-ingbrobu.js b/packages/sync-server/src/app-gocardless/banks/ing-ingbrobu.js new file mode 100644 index 00000000000..bbf39b6f4e7 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/ing-ingbrobu.js @@ -0,0 +1,58 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ING_INGBROBU'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, booked) { + //Merchant transactions all have the same transactionId of 'NOTPROVIDED'. + //For booked transactions, this can be set to the internalTransactionId + //For pending transactions, this needs to be removed for them to show up in Actual + + //For deduplication to work better, payeeName needs to be standardized + //and converted from a pending transaction form ("payeeName":"Card no: xxxxxxxxxxxx1111"') to a booked transaction form ("payeeName":"Card no: Xxxx Xxxx Xxxx 1111") + if (transaction.transactionId === 'NOTPROVIDED') { + if (booked) { + transaction.transactionId = transaction.internalTransactionId; + if ( + transaction.remittanceInformationUnstructured + .toLowerCase() + .includes('card no:') + ) { + transaction.creditorName = + transaction.remittanceInformationUnstructured.split(',')[0]; + //Catch all case for other types of payees + } else { + transaction.creditorName = + transaction.remittanceInformationUnstructured; + } + } else { + transaction.transactionId = null; + if ( + transaction.remittanceInformationUnstructured + .toLowerCase() + .includes('card no:') + ) { + transaction.creditorName = + transaction.remittanceInformationUnstructured.replace( + /x{4}/g, + 'Xxxx ', + ); + //Catch all case for other types of payees + } else { + transaction.creditorName = + transaction.remittanceInformationUnstructured; + } + //Remove remittanceInformationUnstructured from pending transactions, so the `notes` field remains empty (there is no merchant information) + //Once booked, the right `notes` (containing the merchant) will be populated + transaction.remittanceInformationUnstructured = null; + } + } + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/ing-ingddeff.js b/packages/sync-server/src/app-gocardless/banks/ing-ingddeff.js new file mode 100644 index 00000000000..2fecdaa24cd --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/ing-ingddeff.js @@ -0,0 +1,64 @@ +import Fallback from './integration-bank.js'; + +import { printIban, amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ING_INGDDEFF'], + + accessValidForDays: 180, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: account.iban.slice(-4), + iban: account.iban, + name: [account.product, printIban(account)].join(' '), + official_name: account.product, + type: 'checking', + }; + }, + + normalizeTransaction(transaction, _booked) { + const remittanceInformationMatch = /remittanceinformation:(.*)$/.exec( + transaction.remittanceInformationUnstructured, + ); + + transaction.remittanceInformationUnstructured = remittanceInformationMatch + ? remittanceInformationMatch[1] + : transaction.remittanceInformationUnstructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + sortTransactions(transactions = []) { + return transactions.sort((a, b) => { + const diff = + +new Date(b.valueDate || b.bookingDate) - + +new Date(a.valueDate || a.bookingDate); + if (diff) return diff; + const idA = parseInt(a.transactionId); + const idB = parseInt(b.transactionId); + if (!isNaN(idA) && !isNaN(idB)) return idB - idA; + return 0; + }); + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimBooked' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/ing-pl-ingbplpw.js b/packages/sync-server/src/app-gocardless/banks/ing-pl-ingbplpw.js new file mode 100644 index 00000000000..05b7b9ddea3 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/ing-pl-ingbplpw.js @@ -0,0 +1,61 @@ +import Fallback from './integration-bank.js'; + +import { printIban, amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ING_PL_INGBPLPW'], + + accessValidForDays: 180, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: account.iban.slice(-4), + iban: account.iban, + name: [account.product, printIban(account)].join(' ').trim(), + official_name: account.product, + type: 'checking', + }; + }, + + normalizeTransaction(transaction, _booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDate ?? transaction.bookingDate, + }; + }, + + sortTransactions(transactions = []) { + return transactions.sort((a, b) => { + return ( + Number(b.transactionId.substr(2)) - Number(a.transactionId.substr(2)) + ); + }); + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + if (sortedTransactions.length) { + const oldestTransaction = + sortedTransactions[sortedTransactions.length - 1]; + const oldestKnownBalance = amountToInteger( + oldestTransaction.balanceAfterTransaction.balanceAmount.amount, + ); + const oldestTransactionAmount = amountToInteger( + oldestTransaction.transactionAmount.amount, + ); + + return oldestKnownBalance - oldestTransactionAmount; + } else { + return amountToInteger( + balances.find((balance) => 'interimBooked' === balance.balanceType) + .balanceAmount.amount, + ); + } + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js b/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js new file mode 100644 index 00000000000..4f90da51c9d --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js @@ -0,0 +1,70 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ING_INGBROBU'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, booked) { + //Merchant transactions all have the same transactionId of 'NOTPROVIDED'. + //For booked transactions, this can be set to the internalTransactionId + //For pending transactions, this needs to be removed for them to show up in Actual + + //For deduplication to work better, payeeName needs to be standardized + //and converted from a pending transaction form ("payeeName":"Card no: xxxxxxxxxxxx1111"') to a booked transaction form ("payeeName":"Card no: Xxxx Xxxx Xxxx 1111") + if (transaction.transactionId === 'NOTPROVIDED') { + //Some corner case transactions only have the `proprietaryBankTransactionCode` field, this need to be copied to `remittanceInformationUnstructured` + if ( + transaction.proprietaryBankTransactionCode && + !transaction.remittanceInformationUnstructured + ) { + transaction.remittanceInformationUnstructured = + transaction.proprietaryBankTransactionCode; + } + + if (booked) { + transaction.transactionId = transaction.internalTransactionId; + if ( + transaction.remittanceInformationUnstructured && + transaction.remittanceInformationUnstructured + .toLowerCase() + .includes('card no:') + ) { + transaction.creditorName = + transaction.remittanceInformationUnstructured.split(',')[0]; + //Catch all case for other types of payees + } else { + transaction.creditorName = + transaction.remittanceInformationUnstructured; + } + } else { + transaction.transactionId = null; + + if ( + transaction.remittanceInformationUnstructured && + transaction.remittanceInformationUnstructured + .toLowerCase() + .includes('card no:') + ) { + transaction.creditorName = + transaction.remittanceInformationUnstructured.replace( + /x{4}/g, + 'Xxxx ', + ); + //Catch all case for other types of payees + } else { + transaction.creditorName = + transaction.remittanceInformationUnstructured; + } + //Remove remittanceInformationUnstructured from pending transactions, so the `notes` field remains empty (there is no merchant information) + //Once booked, the right `notes` (containing the merchant) will be populated + transaction.remittanceInformationUnstructured = null; + } + } + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js b/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js new file mode 100644 index 00000000000..3eabb990845 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js @@ -0,0 +1,52 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ING_INGDDEFF'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + const remittanceInformationMatch = /remittanceinformation:(.*)$/.exec( + transaction.remittanceInformationUnstructured, + ); + + transaction.remittanceInformationUnstructured = remittanceInformationMatch + ? remittanceInformationMatch[1] + : transaction.remittanceInformationUnstructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + sortTransactions(transactions = []) { + return transactions.sort((a, b) => { + const diff = + +new Date(b.valueDate || b.bookingDate) - + +new Date(a.valueDate || a.bookingDate); + if (diff) return diff; + const idA = parseInt(a.transactionId); + const idB = parseInt(b.transactionId); + if (!isNaN(idA) && !isNaN(idB)) return idB - idA; + return 0; + }); + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimBooked' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js b/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js new file mode 100644 index 00000000000..248068cb84d --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js @@ -0,0 +1,49 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ING_PL_INGBPLPW'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDate ?? transaction.bookingDate, + }; + }, + + sortTransactions(transactions = []) { + return transactions.sort((a, b) => { + return ( + Number(b.transactionId.substr(2)) - Number(a.transactionId.substr(2)) + ); + }); + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + if (sortedTransactions.length) { + const oldestTransaction = + sortedTransactions[sortedTransactions.length - 1]; + const oldestKnownBalance = amountToInteger( + oldestTransaction.balanceAfterTransaction.balanceAmount.amount, + ); + const oldestTransactionAmount = amountToInteger( + oldestTransaction.transactionAmount.amount, + ); + + return oldestKnownBalance - oldestTransactionAmount; + } else { + return amountToInteger( + balances.find((balance) => 'interimBooked' === balance.balanceType) + .balanceAmount.amount, + ); + } + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/integration-bank.js b/packages/sync-server/src/app-gocardless/banks/integration-bank.js new file mode 100644 index 00000000000..c96b0850196 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/integration-bank.js @@ -0,0 +1,102 @@ +import * as d from 'date-fns'; +import { + amountToInteger, + printIban, + sortByBookingDateOrValueDate, +} from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +const SORTED_BALANCE_TYPE_LIST = [ + 'closingBooked', + 'expected', + 'forwardAvailable', + 'interimAvailable', + 'interimBooked', + 'nonInvoiced', + 'openingBooked', +]; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + institutionIds: ['IntegrationBank'], + + // EEA need to allow at least 180 days now but this doesn't apply to UK + // banks, and it's possible that there are EEA banks which still don't follow + // the new requirements. See: + // - https://nordigen.zendesk.com/hc/en-gb/articles/13239212055581-EEA-180-day-access + // - https://nordigen.zendesk.com/hc/en-gb/articles/6760902653085-Extended-history-and-continuous-access-edge-cases + accessValidForDays: 90, + + normalizeAccount(account) { + console.debug( + 'Available account properties for new institution integration', + { account: JSON.stringify(account) }, + ); + + return { + account_id: account.id, + institution: account.institution, + mask: (account?.iban || '0000').slice(-4), + iban: account?.iban || null, + name: [ + account.name ?? account.displayName ?? account.product, + printIban(account), + account.currency, + ] + .filter(Boolean) + .join(' '), + official_name: account.product ?? `integration-${account.institution_id}`, + type: 'checking', + }; + }, + + normalizeTransaction(transaction, _booked) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + }, + + sortTransactions(transactions = []) { + console.debug( + 'Available (first 10) transactions properties for new integration of institution in sortTransactions function', + { top10Transactions: JSON.stringify(transactions.slice(0, 10)) }, + ); + return sortByBookingDateOrValueDate(transactions); + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + console.debug( + 'Available (first 10) transactions properties for new integration of institution in calculateStartingBalance function', + { + balances: JSON.stringify(balances), + top10SortedTransactions: JSON.stringify( + sortedTransactions.slice(0, 10), + ), + }, + ); + + const currentBalance = balances + .filter((item) => SORTED_BALANCE_TYPE_LIST.includes(item.balanceType)) + .sort( + (a, b) => + SORTED_BALANCE_TYPE_LIST.indexOf(a.balanceType) - + SORTED_BALANCE_TYPE_LIST.indexOf(b.balanceType), + )[0]; + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance?.balanceAmount?.amount || 0)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/isybank-itbbitmm.js b/packages/sync-server/src/app-gocardless/banks/isybank-itbbitmm.js new file mode 100644 index 00000000000..a6685ccae02 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/isybank-itbbitmm.js @@ -0,0 +1,16 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ISYBANK_ITBBITMM'], + + // It has been reported that valueDate is more accurate than booking date + // when it is provided + normalizeTransaction(transaction, booked) { + transaction.bookingDate = transaction.valueDate ?? transaction.bookingDate; + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js b/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js new file mode 100644 index 00000000000..cc7e78ac399 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js @@ -0,0 +1,18 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['ISYBANK_ITBBITMM'], + + accessValidForDays: 180, + + // It has been reported that valueDate is more accurate than booking date + // when it is provided + normalizeTransaction(transaction, booked) { + transaction.bookingDate = transaction.valueDate ?? transaction.bookingDate; + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js b/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js new file mode 100644 index 00000000000..6818a2f5583 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js @@ -0,0 +1,39 @@ +import { extractPayeeNameFromRemittanceInfo } from './util/extract-payeeName-from-remittanceInfo.js'; +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['KBC_KREDBEBB'], + + accessValidForDays: 180, + + /** + * For negative amounts, the only payee information we have is returned in + * remittanceInformationUnstructured. + */ + normalizeTransaction(transaction, _booked) { + if (Number(transaction.transactionAmount.amount) > 0) { + return { + ...transaction, + payeeName: + transaction.debtorName || + transaction.remittanceInformationUnstructured || + 'undefined', + date: transaction.bookingDate || transaction.valueDate, + }; + } + + return { + ...transaction, + payeeName: + transaction.creditorName || + extractPayeeNameFromRemittanceInfo( + transaction.remittanceInformationUnstructured, + ['Betaling met', 'Domiciliëring', 'Overschrijving'], + ), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/mbank-retail-brexplpw.js b/packages/sync-server/src/app-gocardless/banks/mbank-retail-brexplpw.js new file mode 100644 index 00000000000..c2f84ad326e --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/mbank-retail-brexplpw.js @@ -0,0 +1,57 @@ +import Fallback from './integration-bank.js'; + +import { printIban, amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['MBANK_RETAIL_BREXPLPW'], + + accessValidForDays: 179, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: account.iban.slice(-4), + iban: account.iban, + name: [account.displayName, printIban(account)].join(' '), + official_name: account.product, + type: 'checking', + }; + }, + + normalizeTransaction(transaction, _booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + sortTransactions(transactions = []) { + return transactions.sort( + (a, b) => Number(b.transactionId) - Number(a.transactionId), + ); + }, + + /** + * For MBANK_RETAIL_BREXPLPW we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `interimBooked` balance type because + * it includes transaction placed during current day + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimBooked' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js b/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js new file mode 100644 index 00000000000..2f6e3c943bf --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js @@ -0,0 +1,45 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['MBANK_RETAIL_BREXPLPW'], + + accessValidForDays: 179, + + normalizeTransaction(transaction, _booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + sortTransactions(transactions = []) { + return transactions.sort( + (a, b) => Number(b.transactionId) - Number(a.transactionId), + ); + }, + + /** + * For MBANK_RETAIL_BREXPLPW we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `interimBooked` balance type because + * it includes transaction placed during current day + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimBooked' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/nationwide-naiagb21.js b/packages/sync-server/src/app-gocardless/banks/nationwide-naiagb21.js new file mode 100644 index 00000000000..90a4c81a3ba --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/nationwide-naiagb21.js @@ -0,0 +1,46 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['NATIONWIDE_NAIAGB21'], + + accessValidForDays: 90, + + normalizeTransaction(transaction, booked) { + // Nationwide can sometimes return pending transactions with a date + // representing the latest a transaction could be booked. This stops + // actual's deduplication logic from working as it only checks 7 days + // ahead/behind and the transactionID from Nationwide changes when a + // transaction is booked + if (!booked) { + const useDate = new Date( + Math.min( + new Date(transaction.bookingDate).getTime(), + new Date().getTime(), + ), + ); + transaction.bookingDate = useDate.toISOString().slice(0, 10); + } + + // Nationwide also occasionally returns erroneous transaction_ids + // that are malformed and can even change after import. This will ignore + // these ids and unset them. When a correct ID is returned then it will + // update via the deduplication logic + const debitCreditRegex = /^00(DEB|CRED)IT.+$/; + const validLengths = [ + 40, // Nationwide credit cards + 32, // Nationwide current accounts + ]; + + if ( + transaction.transactionId?.match(debitCreditRegex) || + !validLengths.includes(transaction.transactionId?.length) + ) { + transaction.transactionId = null; + } + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js b/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js new file mode 100644 index 00000000000..fdcb933527d --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js @@ -0,0 +1,44 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['NATIONWIDE_NAIAGB21'], + + normalizeTransaction(transaction, booked) { + // Nationwide can sometimes return pending transactions with a date + // representing the latest a transaction could be booked. This stops + // actual's deduplication logic from working as it only checks 7 days + // ahead/behind and the transactionID from Nationwide changes when a + // transaction is booked + if (!booked) { + const useDate = new Date( + Math.min( + new Date(transaction.bookingDate).getTime(), + new Date().getTime(), + ), + ); + transaction.bookingDate = useDate.toISOString().slice(0, 10); + } + + // Nationwide also occasionally returns erroneous transaction_ids + // that are malformed and can even change after import. This will ignore + // these ids and unset them. When a correct ID is returned then it will + // update via the deduplication logic + const debitCreditRegex = /^00(DEB|CRED)IT.+$/; + const validLengths = [ + 40, // Nationwide credit cards + 32, // Nationwide current accounts + ]; + + if ( + transaction.transactionId?.match(debitCreditRegex) || + !validLengths.includes(transaction.transactionId?.length) + ) { + transaction.transactionId = null; + } + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js b/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js new file mode 100644 index 00000000000..b915ee3bdf3 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js @@ -0,0 +1,59 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['NBG_ETHNGRAAXXX'], + + accessValidForDays: 180, + + /** + * Fixes for the pending transactions: + * - Corrects amount to negative (nbg erroneously omits the minus sign in pending transactions) + * - Removes prefix 'ΑΓΟΡΑ' from remittance information to align with the booked transaction (necessary for fuzzy matching to work) + */ + normalizeTransaction(transaction, _booked) { + if ( + !transaction.transactionId && + transaction.remittanceInformationUnstructured.startsWith('ΑΓΟΡΑ ') + ) { + transaction = { + ...transaction, + transactionAmount: { + amount: '-' + transaction.transactionAmount.amount, + currency: transaction.transactionAmount.currency, + }, + remittanceInformationUnstructured: + transaction.remittanceInformationUnstructured.substring(6), + }; + } + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + /** + * For NBG_ETHNGRAAXXX we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `interimBooked` balance type because + * it includes transaction placed during current day + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimAvailable' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/norwegian-xx-norwnok1.js b/packages/sync-server/src/app-gocardless/banks/norwegian-xx-norwnok1.js new file mode 100644 index 00000000000..6e8404e0424 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/norwegian-xx-norwnok1.js @@ -0,0 +1,97 @@ +import Fallback from './integration-bank.js'; + +import { printIban, amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: [ + 'NORWEGIAN_NO_NORWNOK1', + 'NORWEGIAN_SE_NORWNOK1', + 'NORWEGIAN_DE_NORWNOK1', + 'NORWEGIAN_DK_NORWNOK1', + 'NORWEGIAN_ES_NORWNOK1', + 'NORWEGIAN_FI_NORWNOK1', + ], + + accessValidForDays: 180, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: account.iban.slice(-4), + iban: account.iban, + name: [account.name, printIban(account)].join(' '), + official_name: account.product, + type: 'checking', + }; + }, + + normalizeTransaction(transaction, booked) { + if (booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate, + }; + } + + /** + * For pending transactions there are two possibilities: + * + * - Either a `valueDate` was set, in which case it corresponds to when the + * transaction actually occurred, or + * - There is no date field, in which case we try to parse the correct date + * out of the `remittanceInformationStructured` field. + * + * If neither case succeeds then we return `null` causing this transaction + * to be filtered out for now, and hopefully we'll be able to import it + * once the bank has processed it further. + */ + if (transaction.valueDate !== undefined) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDate, + }; + } + + if (transaction.remittanceInformationStructured) { + const remittanceInfoRegex = / (\d{4}-\d{2}-\d{2}) /; + const matches = + transaction.remittanceInformationStructured.match(remittanceInfoRegex); + if (matches) { + transaction.valueDate = matches[1]; + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: matches[1], + }; + } + } + + return null; + }, + + /** + * For NORWEGIAN_XX_NORWNOK1 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `expected` balance type because it + * corresponds to the current running balance, whereas `interimAvailable` + * holds the remaining credit limit. + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'expected' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js b/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js new file mode 100644 index 00000000000..0a00a34e988 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js @@ -0,0 +1,85 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: [ + 'NORWEGIAN_NO_NORWNOK1', + 'NORWEGIAN_SE_NORWNOK1', + 'NORWEGIAN_DE_NORWNOK1', + 'NORWEGIAN_DK_NORWNOK1', + 'NORWEGIAN_ES_NORWNOK1', + 'NORWEGIAN_FI_NORWNOK1', + ], + + accessValidForDays: 180, + + normalizeTransaction(transaction, booked) { + if (booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate, + }; + } + + /** + * For pending transactions there are two possibilities: + * + * - Either a `valueDate` was set, in which case it corresponds to when the + * transaction actually occurred, or + * - There is no date field, in which case we try to parse the correct date + * out of the `remittanceInformationStructured` field. + * + * If neither case succeeds then we return `null` causing this transaction + * to be filtered out for now, and hopefully we'll be able to import it + * once the bank has processed it further. + */ + if (transaction.valueDate !== undefined) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDate, + }; + } + + if (transaction.remittanceInformationStructured) { + const remittanceInfoRegex = / (\d{4}-\d{2}-\d{2}) /; + const matches = + transaction.remittanceInformationStructured.match(remittanceInfoRegex); + if (matches) { + transaction.valueDate = matches[1]; + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: matches[1], + }; + } + } + + return null; + }, + + /** + * For NORWEGIAN_XX_NORWNOK1 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `expected` balance type because it + * corresponds to the current running balance, whereas `interimAvailable` + * holds the remaining credit limit. + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'expected' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js b/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js new file mode 100644 index 00000000000..61b27c5a24a --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js @@ -0,0 +1,60 @@ +import { formatPayeeName } from '../../util/payee-name.js'; +import * as d from 'date-fns'; +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['REVOLUT_REVOLT21'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + if ( + transaction.remittanceInformationUnstructuredArray[0].startsWith( + 'Bizum payment from: ', + ) + ) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + + return { + ...transaction, + payeeName: + transaction.remittanceInformationUnstructuredArray[0].replace( + 'Bizum payment from: ', + '', + ), + remittanceInformationUnstructured: + transaction.remittanceInformationUnstructuredArray[1], + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + } + + if ( + transaction.remittanceInformationUnstructuredArray[0].startsWith( + 'Bizum payment to: ', + ) + ) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + remittanceInformationUnstructured: + transaction.remittanceInformationUnstructuredArray[1], + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + } + + return Fallback.normalizeTransaction(transaction, _booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/sandboxfinance-sfin0000.js b/packages/sync-server/src/app-gocardless/banks/sandboxfinance-sfin0000.js new file mode 100644 index 00000000000..015886b038f --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/sandboxfinance-sfin0000.js @@ -0,0 +1,59 @@ +import Fallback from './integration-bank.js'; + +import { printIban, amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SANDBOXFINANCE_SFIN0000'], + + accessValidForDays: 90, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: account.iban.slice(-4), + iban: account.iban, + name: [account.name, printIban(account)].join(' '), + official_name: account.product, + type: 'checking', + }; + }, + + /** + * Following the GoCardless documentation[0] we should prefer `bookingDate` + * here, though some of their bank integrations uses the date field + * differently from what's described in their documentation and so it's + * sometimes necessary to use `valueDate` instead. + * + * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions + */ + normalizeTransaction(transaction, _booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + /** + * For SANDBOXFINANCE_SFIN0000 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `interimBooked` balance type because + * it includes transaction placed during current day + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimAvailable' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js b/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js new file mode 100644 index 00000000000..0debc603062 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js @@ -0,0 +1,47 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SANDBOXFINANCE_SFIN0000'], + + accessValidForDays: 180, + + /** + * Following the GoCardless documentation[0] we should prefer `bookingDate` + * here, though some of their bank integrations uses the date field + * differently from what's described in their documentation and so it's + * sometimes necessary to use `valueDate` instead. + * + * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions + */ + normalizeTransaction(transaction, _booked) { + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + /** + * For SANDBOXFINANCE_SFIN0000 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `interimBooked` balance type because + * it includes transaction placed during current day + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimAvailable' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/seb-kort-bank-ab.js b/packages/sync-server/src/app-gocardless/banks/seb-kort-bank-ab.js new file mode 100644 index 00000000000..bdb606759df --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/seb-kort-bank-ab.js @@ -0,0 +1,71 @@ +import Fallback from './integration-bank.js'; + +import { printIban, amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: [ + 'SEB_KORT_AB_NO_SKHSFI21', + 'SEB_KORT_AB_SE_SKHSFI21', + 'SEB_CARD_ESSESESS', + ], + + accessValidForDays: 180, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: account.iban.slice(-4), + iban: account.iban, + name: [account.name, printIban(account)].join(' '), + official_name: account.product, + type: 'checking', + }; + }, + + /** + * Sign of transaction amount needs to be flipped for SEB credit cards + */ + normalizeTransaction(transaction, _booked) { + // Creditor name is stored in additionInformation for SEB + transaction.creditorName = transaction.additionalInformation; + transaction.transactionAmount = { + // Flip transaction amount sign + amount: (-parseFloat(transaction.transactionAmount.amount)).toString(), + currency: transaction.transactionAmount.currency, + }; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDate, + }; + }, + + /** + * For SEB_KORT_AB_NO_SKHSFI21 and SEB_KORT_AB_SE_SKHSFI21 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `expected` and `nonInvoiced` balance types because it + * corresponds to the current running balance, whereas `interimAvailable` + * holds the remaining credit limit. + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'expected' === balance.balanceType, + ); + + const nonInvoiced = balances.find( + (balance) => 'nonInvoiced' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, -amountToInteger(currentBalance.balanceAmount.amount) + amountToInteger(nonInvoiced.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/seb-privat.js b/packages/sync-server/src/app-gocardless/banks/seb-privat.js new file mode 100644 index 00000000000..0ff079ebf80 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/seb-privat.js @@ -0,0 +1,47 @@ +import Fallback from './integration-bank.js'; + +import * as d from 'date-fns'; +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SEB_ESSESESS_PRIVATE'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + // Creditor name is stored in additionInformation for SEB + transaction.creditorName = transaction.additionalInformation; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimBooked' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js b/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js new file mode 100644 index 00000000000..3b465641f21 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js @@ -0,0 +1,59 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: [ + 'SEB_KORT_AB_NO_SKHSFI21', + 'SEB_KORT_AB_SE_SKHSFI21', + 'SEB_CARD_ESSESESS', + ], + + accessValidForDays: 180, + + /** + * Sign of transaction amount needs to be flipped for SEB credit cards + */ + normalizeTransaction(transaction, _booked) { + // Creditor name is stored in additionInformation for SEB + transaction.creditorName = transaction.additionalInformation; + transaction.transactionAmount = { + // Flip transaction amount sign + amount: (-parseFloat(transaction.transactionAmount.amount)).toString(), + currency: transaction.transactionAmount.currency, + }; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.valueDate, + }; + }, + + /** + * For SEB_KORT_AB_NO_SKHSFI21 and SEB_KORT_AB_SE_SKHSFI21 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `expected` and `nonInvoiced` balance types because it + * corresponds to the current running balance, whereas `interimAvailable` + * holds the remaining credit limit. + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'expected' === balance.balanceType, + ); + + const nonInvoiced = balances.find( + (balance) => 'nonInvoiced' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, -amountToInteger(currentBalance.balanceAmount.amount) + amountToInteger(nonInvoiced.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/seb_privat.js b/packages/sync-server/src/app-gocardless/banks/seb_privat.js new file mode 100644 index 00000000000..0ff079ebf80 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/seb_privat.js @@ -0,0 +1,47 @@ +import Fallback from './integration-bank.js'; + +import * as d from 'date-fns'; +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SEB_ESSESESS_PRIVATE'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + // Creditor name is stored in additionInformation for SEB + transaction.creditorName = transaction.additionalInformation; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + }, + + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimBooked' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/sparnord-spnodk22.js b/packages/sync-server/src/app-gocardless/banks/sparnord-spnodk22.js new file mode 100644 index 00000000000..37980fafbb4 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/sparnord-spnodk22.js @@ -0,0 +1,30 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: [ + 'SPARNORD_SPNODK22', + 'LAGERNES_BANK_LAPNDKK1', + 'ANDELSKASSEN_FALLESKASSEN_FAELDKK1', + ], + + accessValidForDays: 180, + + /** + * Banks on the BEC backend only give information regarding the transaction in additionalInformation + */ + normalizeTransaction(transaction, _booked) { + transaction.remittanceInformationUnstructured = + transaction.additionalInformation; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js b/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js new file mode 100644 index 00000000000..37980fafbb4 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js @@ -0,0 +1,30 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: [ + 'SPARNORD_SPNODK22', + 'LAGERNES_BANK_LAPNDKK1', + 'ANDELSKASSEN_FALLESKASSEN_FAELDKK1', + ], + + accessValidForDays: 180, + + /** + * Banks on the BEC backend only give information regarding the transaction in additionalInformation + */ + normalizeTransaction(transaction, _booked) { + transaction.remittanceInformationUnstructured = + transaction.additionalInformation; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/spk-karlsruhe-karsde66.js b/packages/sync-server/src/app-gocardless/banks/spk-karlsruhe-karsde66.js new file mode 100644 index 00000000000..317d63ec0e2 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/spk-karlsruhe-karsde66.js @@ -0,0 +1,98 @@ +import Fallback from './integration-bank.js'; + +import { printIban, amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SPK_KARLSRUHE_KARSDE66XXX'], + + accessValidForDays: 90, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: account.iban.slice(-4), + iban: account.iban, + name: [account.name, printIban(account)].join(' '), + official_name: account.product, + type: 'checking', + }; + }, + + /** + * Following the GoCardless documentation[0] we should prefer `bookingDate` + * here, though some of their bank integrations uses the date field + * differently from what's described in their documentation and so it's + * sometimes necessary to use `valueDate` instead. + * + * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions + */ + normalizeTransaction(transaction, _booked) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + let remittanceInformationUnstructured; + + if (transaction.remittanceInformationUnstructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured; + } else if (transaction.remittanceInformationStructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructured; + } else if (transaction.remittanceInformationStructuredArray?.length > 0) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructuredArray?.join(' '); + } + + if (transaction.additionalInformation) + remittanceInformationUnstructured += + ' ' + transaction.additionalInformation; + + const usefulCreditorName = + transaction.ultimateCreditor || + transaction.creditorName || + transaction.debtorName; + + transaction.creditorName = usefulCreditorName; + transaction.remittanceInformationUnstructured = + remittanceInformationUnstructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + /** + * For SANDBOXFINANCE_SFIN0000 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `interimBooked` balance type because + * it includes transaction placed during current day + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimAvailable' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/spk-marburg-biedenkopf-heladef1mar.js b/packages/sync-server/src/app-gocardless/banks/spk-marburg-biedenkopf-heladef1mar.js new file mode 100644 index 00000000000..70615bb76a9 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/spk-marburg-biedenkopf-heladef1mar.js @@ -0,0 +1,65 @@ +import Fallback from './integration-bank.js'; + +import d from 'date-fns'; +import { printIban } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SPK_MARBURG_BIEDENKOPF_HELADEF1MAR'], + + accessValidForDays: 180, + + normalizeAccount(account) { + return { + account_id: account.id, + institution: account.institution, + mask: (account?.iban || '0000').slice(-4), + iban: account?.iban || null, + name: [account.product, printIban(account), account.currency] + .filter(Boolean) + .join(' '), + official_name: account.product, + type: 'checking', + }; + }, + + normalizeTransaction(transaction, _booked) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + let remittanceInformationUnstructured; + + if (transaction.remittanceInformationUnstructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured; + } else if (transaction.remittanceInformationStructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructured; + } else if (transaction.remittanceInformationStructuredArray?.length > 0) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructuredArray?.join(' '); + } + + transaction.remittanceInformationUnstructured = + remittanceInformationUnstructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/spk-worms-alzey-ried-malade51wor.js b/packages/sync-server/src/app-gocardless/banks/spk-worms-alzey-ried-malade51wor.js new file mode 100644 index 00000000000..fb45cee0d73 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/spk-worms-alzey-ried-malade51wor.js @@ -0,0 +1,29 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SPK_WORMS_ALZEY_RIED_MALADE51WOR'], + + accessValidForDays: 90, + + normalizeTransaction(transaction, _booked) { + const date = transaction.bookingDate || transaction.valueDate; + if (!date) { + return null; + } + + transaction.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured ?? + transaction.remittanceInformationStructured ?? + transaction.remittanceInformationStructuredArray?.join(' '); + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js b/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js new file mode 100644 index 00000000000..9b8e6a2153e --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js @@ -0,0 +1,86 @@ +import Fallback from './integration-bank.js'; + +import { amountToInteger } from '../utils.js'; +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SPK_KARLSRUHE_KARSDE66XXX'], + + accessValidForDays: 180, + + /** + * Following the GoCardless documentation[0] we should prefer `bookingDate` + * here, though some of their bank integrations uses the date field + * differently from what's described in their documentation and so it's + * sometimes necessary to use `valueDate` instead. + * + * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions + */ + normalizeTransaction(transaction, _booked) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + let remittanceInformationUnstructured; + + if (transaction.remittanceInformationUnstructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured; + } else if (transaction.remittanceInformationStructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructured; + } else if (transaction.remittanceInformationStructuredArray?.length > 0) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructuredArray?.join(' '); + } + + if (transaction.additionalInformation) + remittanceInformationUnstructured += + ' ' + transaction.additionalInformation; + + const usefulCreditorName = + transaction.ultimateCreditor || + transaction.creditorName || + transaction.debtorName; + + transaction.creditorName = usefulCreditorName; + transaction.remittanceInformationUnstructured = + remittanceInformationUnstructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, + + /** + * For SANDBOXFINANCE_SFIN0000 we don't know what balance was + * after each transaction so we have to calculate it by getting + * current balance from the account and subtract all the transactions + * + * As a current balance we use `interimBooked` balance type because + * it includes transaction placed during current day + */ + calculateStartingBalance(sortedTransactions = [], balances = []) { + const currentBalance = balances.find( + (balance) => 'interimAvailable' === balance.balanceType, + ); + + return sortedTransactions.reduce((total, trans) => { + return total - amountToInteger(trans.transactionAmount.amount); + }, amountToInteger(currentBalance.balanceAmount.amount)); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js b/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js new file mode 100644 index 00000000000..3491b131bbb --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js @@ -0,0 +1,51 @@ +import d from 'date-fns'; + +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SPK_MARBURG_BIEDENKOPF_HELADEF1MAR'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + const date = + transaction.bookingDate || + transaction.bookingDateTime || + transaction.valueDate || + transaction.valueDateTime; + + // If we couldn't find a valid date field we filter out this transaction + // and hope that we will import it again once the bank has processed the + // transaction further. + if (!date) { + return null; + } + + let remittanceInformationUnstructured; + + if (transaction.remittanceInformationUnstructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured; + } else if (transaction.remittanceInformationStructured) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructured; + } else if (transaction.remittanceInformationStructuredArray?.length > 0) { + remittanceInformationUnstructured = + transaction.remittanceInformationStructuredArray?.join(' '); + } + + transaction.remittanceInformationUnstructured = + remittanceInformationUnstructured; + + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js b/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js new file mode 100644 index 00000000000..0d38a0244e1 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js @@ -0,0 +1,29 @@ +import Fallback from './integration-bank.js'; + +import { formatPayeeName } from '../../util/payee-name.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SPK_WORMS_ALZEY_RIED_MALADE51WOR'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + const date = transaction.bookingDate || transaction.valueDate; + if (!date) { + return null; + } + + transaction.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured ?? + transaction.remittanceInformationStructured ?? + transaction.remittanceInformationStructuredArray?.join(' '); + return { + ...transaction, + payeeName: formatPayeeName(transaction), + date: transaction.bookingDate || transaction.valueDate, + }; + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js b/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js new file mode 100644 index 00000000000..2e320d366af --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js @@ -0,0 +1,35 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SSK_DUSSELDORF_DUSSDEDDXXX'], + + accessValidForDays: 180, + + normalizeTransaction(transaction, _booked) { + // Prioritize unstructured information, falling back to structured formats + let remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured ?? + transaction.remittanceInformationStructured ?? + transaction.remittanceInformationStructuredArray?.join(' '); + + if (transaction.additionalInformation) + remittanceInformationUnstructured = + (remittanceInformationUnstructured ?? '') + + ' ' + + transaction.additionalInformation; + + const usefulCreditorName = + transaction.ultimateCreditor || + transaction.creditorName || + transaction.debtorName; + + transaction.creditorName = usefulCreditorName; + transaction.remittanceInformationUnstructured = + remittanceInformationUnstructured; + + return Fallback.normalizeTransaction(transaction, _booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/swedbank-habalv22.js b/packages/sync-server/src/app-gocardless/banks/swedbank-habalv22.js new file mode 100644 index 00000000000..87c745cd1cd --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/swedbank-habalv22.js @@ -0,0 +1,51 @@ +import d from 'date-fns'; + +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SWEDBANK_HABALV22'], + + accessValidForDays: 90, + + /** + * The actual transaction date for card transactions is only available in the remittanceInformationUnstructured field when the transaction is booked. + */ + normalizeTransaction(transaction, booked) { + const isCardTransaction = + transaction.remittanceInformationUnstructured?.startsWith('PIRKUMS'); + + if (isCardTransaction) { + if (!booked && !transaction.creditorName) { + const creditorNameMatch = + transaction.remittanceInformationUnstructured?.match( + /PIRKUMS [\d*]+ \d{2}\.\d{2}\.\d{2} \d{2}:\d{2} [\d.]+ \w{3} \(\d+\) (.+)/, + ); + + if (creditorNameMatch) { + transaction = { + ...transaction, + creditorName: creditorNameMatch[1], + }; + } + } + + const dateMatch = transaction.remittanceInformationUnstructured?.match( + /PIRKUMS [\d*]+ (\d{2}\.\d{2}\.\d{4})/, + ); + + if (dateMatch) { + const extractedDate = d.parse(dateMatch[1], 'dd.MM.yyyy', new Date()); + + transaction = { + ...transaction, + bookingDate: d.format(extractedDate, 'yyyy-MM-dd'), + }; + } + } + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js b/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js new file mode 100644 index 00000000000..8bea1360a7a --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js @@ -0,0 +1,51 @@ +import d from 'date-fns'; + +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['SWEDBANK_HABALV22'], + + accessValidForDays: 180, + + /** + * The actual transaction date for card transactions is only available in the remittanceInformationUnstructured field when the transaction is booked. + */ + normalizeTransaction(transaction, booked) { + const isCardTransaction = + transaction.remittanceInformationUnstructured?.startsWith('PIRKUMS'); + + if (isCardTransaction) { + if (!booked && !transaction.creditorName) { + const creditorNameMatch = + transaction.remittanceInformationUnstructured?.match( + /PIRKUMS [\d*]+ \d{2}\.\d{2}\.\d{2} \d{2}:\d{2} [\d.]+ \w{3} \(\d+\) (.+)/, + ); + + if (creditorNameMatch) { + transaction = { + ...transaction, + creditorName: creditorNameMatch[1], + }; + } + } + + const dateMatch = transaction.remittanceInformationUnstructured?.match( + /PIRKUMS [\d*]+ (\d{2}\.\d{2}\.\d{4})/, + ); + + if (dateMatch) { + const extractedDate = d.parse(dateMatch[1], 'dd.MM.yyyy', new Date()); + + transaction = { + ...transaction, + bookingDate: d.format(extractedDate, 'yyyy-MM-dd'), + }; + } + } + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/banks/tests/FORTUNEO_FTNOFRP1XXX.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/FORTUNEO_FTNOFRP1XXX.spec.js new file mode 100644 index 00000000000..d4a1b30ff74 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/FORTUNEO_FTNOFRP1XXX.spec.js @@ -0,0 +1,210 @@ +import Fortuneo from '../fortuneo_ftnofrp1xxx.js'; + +describe('Fortuneo', () => { + describe('#normalizeTransaction', () => { + const transactionsRaw = [ + { + bookingDate: '2024-07-05', + valueDate: '2024-07-05', + transactionAmount: { + amount: '-12.0', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: ['PRLV ONG'], + internalTransactionId: '674323725470140d5caaf7b85a135817', + date: '2024-07-05', + }, + { + bookingDate: '2024-07-04', + valueDate: '2024-07-04', + transactionAmount: { + amount: '-7.72', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: [ + 'PRLV PRIXTEL SCOR/53766825A', + ], + internalTransactionId: 'e8365f68077f2be249f8dfa9183296e4', + date: '2024-07-04', + }, + { + bookingDate: '2024-07-04', + valueDate: '2024-07-04', + transactionAmount: { + amount: '-500.0', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: ['VIR XXXYYYYZZZ'], + internalTransactionId: '0c12be495b71a63d14e46c43bfcb12f6', + date: '2024-07-04', + }, + { + bookingDate: '2024-07-04', + valueDate: '2024-07-04', + transactionAmount: { + amount: '-10.49', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: [ + 'CARTE 04/07 Google Payment I Dublin', + ], + internalTransactionId: 'b09df9be4711cb06bdd2a53aef5423cc', + date: '2024-07-04', + }, + { + bookingDate: '2024-07-04', + valueDate: '2024-07-04', + transactionAmount: { + amount: '-6.38', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: ['CARTE 03/07 SPORT MARKET'], + internalTransactionId: '67552cc7782c742f1df8297e614470ea', + date: '2024-07-04', + }, + { + bookingDate: '2024-07-04', + valueDate: '2024-07-04', + transactionAmount: { + amount: '26.52', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: [ + 'ANN CARTE WEEZEVENT SOMEPLACE', + ], + internalTransactionId: 'c0bed1b61806bd45fd07732e5dfb1f11', + date: '2024-07-04', + }, + { + bookingDate: '2024-07-03', + valueDate: '2024-07-03', + transactionAmount: { + amount: '-104.9', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: [ + "CARTE 02/07 HPY*L'APPAC - Sport JANDA", + ], + internalTransactionId: '7716b23b56cda848efd788a0d8c79d12', + date: '2024-07-03', + }, + { + bookingDate: '2024-07-03', + valueDate: '2024-07-02', + transactionAmount: { + amount: '-22.95', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: [ + 'VIR INST Leclerc XXXX Leclerc XXXX 44321IXCRT211141232', + ], + internalTransactionId: 'e75304593c9557f20014904f90eb23a2', + date: '2024-07-03', + }, + { + bookingDate: '2024-07-02', + valueDate: '2024-07-02', + transactionAmount: { + amount: '-8.9', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: ['CARTE 01/07 CHIK CHAK'], + internalTransactionId: 'e9811e50c8d7453c459f4e42453cf07c', + date: '2024-07-02', + }, + { + bookingDate: '2024-07-02', + valueDate: '2024-07-02', + transactionAmount: { + amount: '-8.0', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: [ + 'CARTE 01/07 SERVICE 1228 GENEV 8,00 EUR', + ], + internalTransactionId: '354a49232bd05de583a3d2ab834e20cd', + date: '2024-07-02', + }, + ]; + + it('sets debtor and creditor name according to amount', () => { + const creditorTransaction = transactionsRaw[0]; + const debtorTransaction = transactionsRaw[5]; + + const normalizedCreditorTransaction = Fortuneo.normalizeTransaction( + creditorTransaction, + true, + ); + const normalizedDebtorTransaction = Fortuneo.normalizeTransaction( + debtorTransaction, + true, + ); + + expect(normalizedCreditorTransaction.creditorName).toBeDefined(); + expect(normalizedCreditorTransaction.debtorName).toBeNull(); + expect( + parseFloat(normalizedCreditorTransaction.transactionAmount.amount), + ).toBeLessThan(0); + + expect(normalizedDebtorTransaction.debtorName).toBeDefined(); + expect(normalizedDebtorTransaction.creditorName).toBeNull(); + expect( + parseFloat(normalizedDebtorTransaction.transactionAmount.amount), + ).toBeGreaterThan(0); + }); + + it('extracts payee name from remittanceInformationUnstructured', () => { + const transaction0 = transactionsRaw[0]; + const normalizedTransaction = Fortuneo.normalizeTransaction( + transaction0, + true, + ); + + expect(normalizedTransaction.creditorName).toBe('ONG'); + + const transaction2 = transactionsRaw[2]; + const normalizedTransaction2 = Fortuneo.normalizeTransaction( + transaction2, + true, + ); + + expect(normalizedTransaction2.creditorName).toBe('XXXYYYYZZZ'); + + const transaction3 = transactionsRaw[3]; + const normalizedTransaction3 = Fortuneo.normalizeTransaction( + transaction3, + true, + ); + + expect(normalizedTransaction3.creditorName).toBe( + 'Google Payment I Dublin', + ); + + const transaction4 = transactionsRaw[4]; + const normalizedTransaction4 = Fortuneo.normalizeTransaction( + transaction4, + true, + ); + + expect(normalizedTransaction4.creditorName).toBe('SPORT MARKET'); + + const transaction5 = transactionsRaw[5]; + const normalizedTransaction5 = Fortuneo.normalizeTransaction( + transaction5, + true, + ); + + expect(normalizedTransaction5.debtorName).toBe('WEEZEVENT SOMEPLACE'); + + const transaction7 = transactionsRaw[7]; + const normalizedTransaction7 = Fortuneo.normalizeTransaction( + transaction7, + true, + ); + + expect(normalizedTransaction7.creditorName).toBe( + 'Leclerc XXXX Leclerc XXXX 44321IXCRT211141232', + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/abanca-caglesmm.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/abanca-caglesmm.spec.js new file mode 100644 index 00000000000..c57961375fb --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/abanca-caglesmm.spec.js @@ -0,0 +1,25 @@ +import Abanca from '../abanca-caglesmm.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('Abanca', () => { + describe('#normalizeTransaction', () => { + it('returns the creditorName and debtorName as remittanceInformationStructured', () => { + const transaction = { + transactionId: 'non-unique-id', + internalTransactionId: 'D202301180000003', + transactionAmount: mockTransactionAmount, + remittanceInformationStructured: 'some-creditor-name', + }; + const normalizedTransaction = Abanca.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.creditorName).toEqual( + transaction.remittanceInformationStructured, + ); + expect(normalizedTransaction.debtorName).toEqual( + transaction.remittanceInformationStructured, + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js new file mode 100644 index 00000000000..3ceb0817c31 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js @@ -0,0 +1,25 @@ +import Abanca from '../abanca_caglesmm.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('Abanca', () => { + describe('#normalizeTransaction', () => { + it('returns the creditorName and debtorName as remittanceInformationStructured', () => { + const transaction = { + transactionId: 'non-unique-id', + internalTransactionId: 'D202301180000003', + transactionAmount: mockTransactionAmount, + remittanceInformationStructured: 'some-creditor-name', + }; + const normalizedTransaction = Abanca.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.creditorName).toEqual( + transaction.remittanceInformationStructured, + ); + expect(normalizedTransaction.debtorName).toEqual( + transaction.remittanceInformationStructured, + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js new file mode 100644 index 00000000000..db93bd2c7f3 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js @@ -0,0 +1,61 @@ +import AbnamroAbnanl2a from '../abnamro_abnanl2a.js'; + +describe('AbnamroAbnanl2a', () => { + describe('#normalizeTransaction', () => { + it('correctly extracts the payee and when not provided', () => { + const transaction = { + transactionId: '0123456789012345', + bookingDate: '2023-12-11', + valueDateTime: '2023-12-09T15:43:37.950', + transactionAmount: { + amount: '-10.00', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: [ + 'BEA, Betaalpas', + 'My Payee Name,PAS123', + 'NR:123A4B, 09.12.23/15:43', + 'CITY', + ], + }; + + const normalizedTransaction = AbnamroAbnanl2a.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + 'BEA, Betaalpas, My Payee Name,PAS123, NR:123A4B, 09.12.23/15:43, CITY', + ); + expect(normalizedTransaction.payeeName).toEqual('My Payee Name'); + }); + + it('correctly extracts the payee for google pay', () => { + const transaction = { + transactionId: '0123456789012345', + bookingDate: '2023-12-11', + valueDateTime: '2023-12-09T15:43:37.950', + transactionAmount: { + amount: '-10.00', + currency: 'EUR', + }, + remittanceInformationUnstructuredArray: [ + 'BEA, Google Pay', + 'CCV*Other payee name,PAS123', + 'NR:123A4B, 09.12.23/15:43', + 'CITY', + ], + }; + + const normalizedTransaction = AbnamroAbnanl2a.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + 'BEA, Google Pay, CCV*Other payee name,PAS123, NR:123A4B, 09.12.23/15:43, CITY', + ); + expect(normalizedTransaction.payeeName).toEqual('Other Payee Name'); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell-bsabesbbb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell-bsabesbbb.spec.js new file mode 100644 index 00000000000..61071084ebc --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell-bsabesbbb.spec.js @@ -0,0 +1,57 @@ +import Sabadell from '../bancsabadell-bsabesbbb.js'; + +describe('BancSabadell', () => { + describe('#normalizeTransaction', () => { + describe('returns the creditorName and debtorName from remittanceInformationUnstructuredArray', () => { + it('debtor role - amount < 0', () => { + const transaction = { + transactionAmount: { amount: '-100', currency: 'EUR' }, + remittanceInformationUnstructuredArray: ['some-creditor-name'], + internalTransactionId: 'd7dca139cf31d9', + transactionId: '04704109322', + bookingDate: '2022-05-01', + }; + const normalizedTransaction = Sabadell.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.creditorName).toEqual( + 'some-creditor-name', + ); + expect(normalizedTransaction.debtorName).toEqual(null); + }); + + it('creditor role - amount > 0', () => { + const transaction = { + transactionAmount: { amount: '100', currency: 'EUR' }, + remittanceInformationUnstructuredArray: ['some-debtor-name'], + internalTransactionId: 'd7dca139cf31d9', + transactionId: '04704109322', + bookingDate: '2022-05-01', + }; + const normalizedTransaction = Sabadell.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.debtorName).toEqual('some-debtor-name'); + expect(normalizedTransaction.creditorName).toEqual(null); + }); + }); + + it('extract date', () => { + const transaction = { + transactionAmount: { amount: '-100', currency: 'EUR' }, + remittanceInformationUnstructuredArray: ['some-creditor-name'], + internalTransactionId: 'd7dca139cf31d9', + transactionId: '04704109322', + bookingDate: '2024-10-02', + valueDate: '2024-10-05', + }; + const normalizedTransaction = Sabadell.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.date).toEqual('2024-10-02'); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js new file mode 100644 index 00000000000..1e7cb914018 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js @@ -0,0 +1,57 @@ +import Sabadell from '../bancsabadell_bsabesbbb.js'; + +describe('BancSabadell', () => { + describe('#normalizeTransaction', () => { + describe('returns the creditorName and debtorName from remittanceInformationUnstructuredArray', () => { + it('debtor role - amount < 0', () => { + const transaction = { + transactionAmount: { amount: '-100', currency: 'EUR' }, + remittanceInformationUnstructuredArray: ['some-creditor-name'], + internalTransactionId: 'd7dca139cf31d9', + transactionId: '04704109322', + bookingDate: '2022-05-01', + }; + const normalizedTransaction = Sabadell.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.creditorName).toEqual( + 'some-creditor-name', + ); + expect(normalizedTransaction.debtorName).toEqual(null); + }); + + it('creditor role - amount > 0', () => { + const transaction = { + transactionAmount: { amount: '100', currency: 'EUR' }, + remittanceInformationUnstructuredArray: ['some-debtor-name'], + internalTransactionId: 'd7dca139cf31d9', + transactionId: '04704109322', + bookingDate: '2022-05-01', + }; + const normalizedTransaction = Sabadell.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.debtorName).toEqual('some-debtor-name'); + expect(normalizedTransaction.creditorName).toEqual(null); + }); + }); + + it('extract date', () => { + const transaction = { + transactionAmount: { amount: '-100', currency: 'EUR' }, + remittanceInformationUnstructuredArray: ['some-creditor-name'], + internalTransactionId: 'd7dca139cf31d9', + transactionId: '04704109322', + bookingDate: '2024-10-02', + valueDate: '2024-10-05', + }; + const normalizedTransaction = Sabadell.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.date).toEqual('2024-10-02'); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js new file mode 100644 index 00000000000..25291a6a897 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js @@ -0,0 +1,21 @@ +import Belfius from '../belfius_gkccbebb.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('Belfius', () => { + describe('#normalizeTransaction', () => { + it('returns the internalTransactionId as transactionId', () => { + const transaction = { + transactionId: 'non-unique-id', + internalTransactionId: 'D202301180000003', + transactionAmount: mockTransactionAmount, + }; + const normalizedTransaction = Belfius.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.transactionId).toEqual( + transaction.internalTransactionId, + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js new file mode 100644 index 00000000000..4114ac4944f --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js @@ -0,0 +1,32 @@ +import CBCcregbebb from '../cbc_cregbebb.js'; + +describe('cbc_cregbebb', () => { + describe('#normalizeTransaction', () => { + it('returns the remittanceInformationUnstructured as payeeName when the amount is negative', () => { + const transaction = { + remittanceInformationUnstructured: + 'ONKART FR Viry Paiement Maestro par Carte de débit CBC 05-09-2024 à 15.43 heures 6703 19XX XXXX X201 5 JOHN DOE', + transactionAmount: { amount: '-45.00', currency: 'EUR' }, + }; + const normalizedTransaction = CBCcregbebb.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.payeeName).toEqual('ONKART FR Viry'); + }); + + it('returns the debtorName as payeeName when the amount is positive', () => { + const transaction = { + debtorName: 'ONKART FR Viry', + remittanceInformationUnstructured: + 'ONKART FR Viry Paiement Maestro par Carte de débit CBC 05-09-2024 à 15.43 heures 6703 19XX XXXX X201 5 JOHN DOE', + transactionAmount: { amount: '10.99', currency: 'EUR' }, + }; + const normalizedTransaction = CBCcregbebb.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.payeeName).toEqual('ONKART FR Viry'); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/easybank-bawaatww.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/easybank-bawaatww.spec.js new file mode 100644 index 00000000000..09363b7a455 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/easybank-bawaatww.spec.js @@ -0,0 +1,54 @@ +import EasybankBawaatww from '../easybank-bawaatww.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('easybank', () => { + describe('#normalizeTransaction', () => { + it('returns the expected payeeName from a transaction with a set creditorName', () => { + const transaction = { + creditorName: 'Some Payee Name', + transactionAmount: mockTransactionAmount, + bookingDate: '2024-01-01', + creditorAccount: 'AT611904300234573201', + }; + + const normalizedTransaction = EasybankBawaatww.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.payeeName).toEqual('Some Payee Name'); + }); + + it('returns the expected payeeName from a transaction with payee name inside structuredInformation', () => { + const transaction = { + payeeName: '', + transactionAmount: mockTransactionAmount, + remittanceInformationStructured: + 'Bezahlung Karte MC/000001234POS 1234 K001 12.12. 23:59SOME PAYEE NAME\\\\LOCATION\\1', + bookingDate: '2023-12-31', + }; + const normalizedTransaction = EasybankBawaatww.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.payeeName).toEqual('Some Payee Name'); + }); + + it('returns the full structured information as payeeName from a transaction with no payee name', () => { + const transaction = { + payeeName: '', + transactionAmount: mockTransactionAmount, + remittanceInformationStructured: + 'Auszahlung Karte MC/000001234AUTOMAT 00012345 K001 31.12. 23:59', + bookingDate: '2023-12-31', + }; + const normalizedTransaction = EasybankBawaatww.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.payeeName).toEqual( + 'Auszahlung Karte MC/000001234AUTOMAT 00012345 K001 31.12. 23:59', + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js new file mode 100644 index 00000000000..c55be72a916 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js @@ -0,0 +1,54 @@ +import EasybankBawaatww from '../easybank_bawaatww.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('easybank', () => { + describe('#normalizeTransaction', () => { + it('returns the expected payeeName from a transaction with a set creditorName', () => { + const transaction = { + creditorName: 'Some Payee Name', + transactionAmount: mockTransactionAmount, + bookingDate: '2024-01-01', + creditorAccount: 'AT611904300234573201', + }; + + const normalizedTransaction = EasybankBawaatww.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.payeeName).toEqual('Some Payee Name'); + }); + + it('returns the expected payeeName from a transaction with payee name inside structuredInformation', () => { + const transaction = { + payeeName: '', + transactionAmount: mockTransactionAmount, + remittanceInformationStructured: + 'Bezahlung Karte MC/000001234POS 1234 K001 12.12. 23:59SOME PAYEE NAME\\\\LOCATION\\1', + bookingDate: '2023-12-31', + }; + const normalizedTransaction = EasybankBawaatww.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.payeeName).toEqual('Some Payee Name'); + }); + + it('returns the full structured information as payeeName from a transaction with no payee name', () => { + const transaction = { + payeeName: '', + transactionAmount: mockTransactionAmount, + remittanceInformationStructured: + 'Auszahlung Karte MC/000001234AUTOMAT 00012345 K001 31.12. 23:59', + bookingDate: '2023-12-31', + }; + const normalizedTransaction = EasybankBawaatww.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.payeeName).toEqual( + 'Auszahlung Karte MC/000001234AUTOMAT 00012345 K001 31.12. 23:59', + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/ing-ingddeff.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/ing-ingddeff.spec.js new file mode 100644 index 00000000000..a3d027c2f0c --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/ing-ingddeff.spec.js @@ -0,0 +1,300 @@ +import IngIngddeff from '../ing-ingddeff.js'; + +describe('IngIngddeff', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + resourceId: 'e896eec6-6096-4efc-a941-756bd9d74765', + iban: 'DE02500105170137075030', + currency: 'EUR', + ownerName: 'Jane Doe', + product: 'Girokonto', + id: 'a787ba27-02ee-4fd6-be86-73831adc5498', + created: '2023-12-29T14:17:11.630352Z', + last_accessed: '2023-12-29T14:19:42.709478Z', + institution_id: 'ING_INGDDEFF', + status: 'READY', + owner_name: 'Jane Doe', + institution: { + id: 'ING_INGDDEFF', + name: 'ING', + bic: 'INGDDEFFXXX', + transaction_total_days: '390', + countries: ['DE'], + logo: 'https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/ing.png', + supported_payments: { + 'single-payment': ['SCT'], + }, + supported_features: [ + 'account_selection', + 'business_accounts', + 'corporate_accounts', + 'payments', + 'pending_transactions', + 'private_accounts', + ], + /*identification_codes: [],*/ + }, + }; + + it('returns normalized account data returned to Frontend', () => { + expect(IngIngddeff.normalizeAccount(accountRaw)).toEqual({ + account_id: 'a787ba27-02ee-4fd6-be86-73831adc5498', + iban: 'DE02500105170137075030', + institution: { + bic: 'INGDDEFFXXX', + countries: ['DE'], + id: 'ING_INGDDEFF', + logo: 'https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/ing.png', + name: 'ING', + supported_features: [ + 'account_selection', + 'business_accounts', + 'corporate_accounts', + 'payments', + 'pending_transactions', + 'private_accounts', + ], + supported_payments: { + 'single-payment': ['SCT'], + }, + transaction_total_days: '390', + }, + mask: '5030', + name: 'Girokonto (XXX 5030)', + official_name: 'Girokonto', + type: 'checking', + }); + }); + }); + + const transactionsRaw = [ + { + transactionId: '000010348081381', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-4.00', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 63053 51590342815 KAUFUMSATZ 24.90 2311825 ARN044873748454374484719431 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '085179a2e5fa34b0ff71b3f2c9f4876f', + date: '2023-12-29', + }, + { + transactionId: '000010348081380', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-2.00', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 8987 90671935362 KAUFUMSATZ 94.81 929614 ARN54795476045598005130492 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '0707bbe2de27e5aabfd5dc614c584951', + date: '2023-12-29', + }, + { + transactionId: '000010348081379', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-6.00', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 2206 17679024325 KAUFUMSATZ 55.25 819456 ARN08595270353806495555431 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '4b15b590652c9ebdc3f974591b15b250', + date: '2023-12-29', + }, + { + transactionId: '000010348081378', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-12.99', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 9437 535-182-825 LU KAUFUMSATZ 43.79 665448 ARN86236748928277201384604 ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: 'f930f8c153f3e37fb9906e4b3a2b4552', + date: '2023-12-29', + }, + { + transactionId: '000010348081377', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-9.00', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 3582 98236826123 KAUFUMSATZ 88.90 477561 ARN64452564252952225664357 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '1ce866282deb78cc4ff4cd108e11b8cc', + date: '2023-12-29', + }, + { + transactionId: '000010347374680', + endToEndId: '9212020-0900000070-2023121711315956', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '2892.61', + currency: 'EUR', + }, + debtorName: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:F22685813 Gehalt 80/6586', + proprietaryBankTransactionCode: 'Gehalt/Rente', + internalTransactionId: 'e731d8eb47f1ae96ccc11e1fb8b76a60', + date: '2023-12-29', + }, + { + transactionId: '000010336959253', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-28', + valueDate: '2023-12-28', + transactionAmount: { + amount: '-85.80', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 7082 FAUCOGNEY E FR KAUFUMSATZ 38.20 265113 ARN47998616225906149245029 ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '2bbc054ae7ba299482a7849fded864f3', + date: '2023-12-28', + }, + { + transactionId: '000010350537843', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-27', + transactionAmount: { + amount: '2.79', + currency: 'EUR', + }, + debtorName: ' ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation: Zins/Dividende ISIN IE36B9RBWM04 VANG.FTSE', + proprietaryBankTransactionCode: 'Zins / Dividende WP', + internalTransactionId: '3bb7c58199d3fa5a44e85871d9001798', + date: '2023-12-29', + }, + { + transactionId: '000010341786083', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-28', + valueDate: '2023-12-27', + transactionAmount: { + amount: '79.80', + currency: 'EUR', + }, + debtorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 4619 GUTSCHRIFTSBELEG 03.91 134870 ', + proprietaryBankTransactionCode: 'Gutschrift', + internalTransactionId: '5570eefb7213e39153a6c7fb97d7dc6f', + date: '2023-12-28', + }, + { + transactionId: '000010328399902', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-27', + valueDate: '2023-12-27', + transactionAmount: { + amount: '-10.90', + currency: 'EUR', + }, + debtorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 3465 XXXXXXXXX KAUFUMSATZ 90.40 505416 ARN63639757770303957985044 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '1b1bf30b23afb56ba4d41b9c65cf0efa', + date: '2023-12-27', + }, + ]; + + describe('#sortTransactions', () => { + it('handles empty arrays', () => { + const transactions = []; + const sortedTransactions = IngIngddeff.sortTransactions(transactions); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = IngIngddeff.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + + it('returns sorted array for unsorted inputs', () => { + const normalizeTransactions = transactionsRaw.map((tx) => + IngIngddeff.normalizeTransaction(tx, true), + ); + const originalOrder = Array.from(normalizeTransactions); + const swap = (a, b) => { + const swap = normalizeTransactions[a]; + normalizeTransactions[a] = normalizeTransactions[b]; + normalizeTransactions[b] = swap; + }; + swap(1, 4); + swap(3, 6); + swap(0, 7); + const sortedTransactions = IngIngddeff.sortTransactions( + normalizeTransactions, + ); + expect(sortedTransactions).toEqual(originalOrder); + }); + }); + + describe('#countStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '3596.87', currency: 'EUR' }, + balanceType: 'interimBooked', + lastChangeDateTime: '2023-12-29T16:44:06.479Z', + }, + ]; + + it('should calculate the starting balance correctly', () => { + const normalizeTransactions = transactionsRaw.map((tx) => + IngIngddeff.normalizeTransaction(tx, true), + ); + const sortedTransactions = IngIngddeff.sortTransactions( + normalizeTransactions, + ); + + const startingBalance = IngIngddeff.calculateStartingBalance( + sortedTransactions, + balances, + ); + + expect(startingBalance).toEqual(75236); + }); + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + expect( + IngIngddeff.calculateStartingBalance(transactions, balances), + ).toEqual(359687); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/ing-pl-ingbplpw.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/ing-pl-ingbplpw.spec.js new file mode 100644 index 00000000000..ac8699ef604 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/ing-pl-ingbplpw.spec.js @@ -0,0 +1,200 @@ +import IngPlIngbplpw from '../ing-pl-ingbplpw.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('IngPlIngbplpw', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + resourceId: 'PL00000000000000000987654321', + iban: 'PL00000000000000000987654321', + currency: 'PLN', + ownerName: 'John Example', + product: 'Current Account for Individuals (Retail)', + bic: 'INGBPLPW', + ownerAddressUnstructured: [ + 'UL. EXAMPLE STREET 10 M.1', + '00-000 WARSZAWA', + ], + id: 'd3eccc94-9536-48d3-98be-813f79199ee3', + created: '2022-07-24T20:45:47.929582Z', + last_accessed: '2023-01-24T22:12:00.193558Z', + institution_id: 'ING_PL_INGBPLPW', + status: 'READY', + owner_name: '', + institution: { + id: 'ING_PL_INGBPLPW', + name: 'ING', + bic: 'INGBPLPW', + transaction_total_days: '365', + countries: ['PL'], + logo: 'https://cdn.nordigen.com/ais/ING_PL_INGBPLPW.png', + supported_payments: {}, + supported_features: [ + 'access_scopes', + 'business_accounts', + 'card_accounts', + 'corporate_accounts', + 'pending_transactions', + 'private_accounts', + ], + }, + }; + + it('returns normalized account data returned to Frontend', () => { + const normalizedAccount = IngPlIngbplpw.normalizeAccount(accountRaw); + expect(normalizedAccount).toMatchInlineSnapshot(` + { + "account_id": "d3eccc94-9536-48d3-98be-813f79199ee3", + "iban": "PL00000000000000000987654321", + "institution": { + "bic": "INGBPLPW", + "countries": [ + "PL", + ], + "id": "ING_PL_INGBPLPW", + "logo": "https://cdn.nordigen.com/ais/ING_PL_INGBPLPW.png", + "name": "ING", + "supported_features": [ + "access_scopes", + "business_accounts", + "card_accounts", + "corporate_accounts", + "pending_transactions", + "private_accounts", + ], + "supported_payments": {}, + "transaction_total_days": "365", + }, + "mask": "4321", + "name": "Current Account for Individuals (Retail) (XXX 4321)", + "official_name": "Current Account for Individuals (Retail)", + "type": "checking", + } + `); + }); + }); + + describe('#sortTransactions', () => { + it('sorts transactions by time and sequence from newest to oldest', () => { + const transactions = [ + { + transactionId: 'D202301180000003', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000004', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301230000001', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000002', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301200000001', + transactionAmount: mockTransactionAmount, + }, + ]; + const sortedTransactions = IngPlIngbplpw.sortTransactions(transactions); + expect(sortedTransactions).toEqual([ + { + transactionId: 'D202301230000001', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301200000001', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000004', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000003', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000002', + transactionAmount: mockTransactionAmount, + }, + ]); + }); + + it('handles empty arrays', () => { + const transactions = []; + const sortedTransactions = IngPlIngbplpw.sortTransactions(transactions); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = IngPlIngbplpw.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + }); + + describe('#countStartingBalance', () => { + it('should calculate the starting balance correctly', () => { + /** @type {import('../../gocardless-node.types.js').Transaction[]} */ + const sortedTransactions = [ + { + transactionAmount: { amount: '-100.00', currency: 'USD' }, + balanceAfterTransaction: { + balanceAmount: { amount: '400.00', currency: 'USD' }, + balanceType: 'interimBooked', + }, + }, + { + transactionAmount: { amount: '50.00', currency: 'USD' }, + balanceAfterTransaction: { + balanceAmount: { amount: '450.00', currency: 'USD' }, + balanceType: 'interimBooked', + }, + }, + { + transactionAmount: { amount: '-25.00', currency: 'USD' }, + balanceAfterTransaction: { + balanceAmount: { amount: '475.00', currency: 'USD' }, + balanceType: 'interimBooked', + }, + }, + ]; + + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceType: 'interimBooked', + balanceAmount: { amount: '500.00', currency: 'USD' }, + }, + { + balanceType: 'closingBooked', + balanceAmount: { amount: '600.00', currency: 'USD' }, + }, + ]; + + const startingBalance = IngPlIngbplpw.calculateStartingBalance( + sortedTransactions, + balances, + ); + + expect(startingBalance).toEqual(50000); + }); + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceType: 'interimBooked', + balanceAmount: { amount: '500.00', currency: 'USD' }, + }, + ]; + expect( + IngPlIngbplpw.calculateStartingBalance(transactions, balances), + ).toEqual(50000); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/ing_ingddeff.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/ing_ingddeff.spec.js new file mode 100644 index 00000000000..156875bdfed --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/ing_ingddeff.spec.js @@ -0,0 +1,300 @@ +import IngIngddeff from '../ing_ingddeff.js'; + +describe('IngIngddeff', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + resourceId: 'e896eec6-6096-4efc-a941-756bd9d74765', + iban: 'DE02500105170137075030', + currency: 'EUR', + ownerName: 'Jane Doe', + product: 'Girokonto', + id: 'a787ba27-02ee-4fd6-be86-73831adc5498', + created: '2023-12-29T14:17:11.630352Z', + last_accessed: '2023-12-29T14:19:42.709478Z', + institution_id: 'ING_INGDDEFF', + status: 'READY', + owner_name: 'Jane Doe', + institution: { + id: 'ING_INGDDEFF', + name: 'ING', + bic: 'INGDDEFFXXX', + transaction_total_days: '390', + countries: ['DE'], + logo: 'https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/ing.png', + supported_payments: { + 'single-payment': ['SCT'], + }, + supported_features: [ + 'account_selection', + 'business_accounts', + 'corporate_accounts', + 'payments', + 'pending_transactions', + 'private_accounts', + ], + /*identification_codes: [],*/ + }, + }; + + it('returns normalized account data returned to Frontend', () => { + expect(IngIngddeff.normalizeAccount(accountRaw)).toEqual({ + account_id: 'a787ba27-02ee-4fd6-be86-73831adc5498', + iban: 'DE02500105170137075030', + institution: { + bic: 'INGDDEFFXXX', + countries: ['DE'], + id: 'ING_INGDDEFF', + logo: 'https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/ing.png', + name: 'ING', + supported_features: [ + 'account_selection', + 'business_accounts', + 'corporate_accounts', + 'payments', + 'pending_transactions', + 'private_accounts', + ], + supported_payments: { + 'single-payment': ['SCT'], + }, + transaction_total_days: '390', + }, + mask: '5030', + name: 'Girokonto (XXX 5030) EUR', + official_name: 'Girokonto', + type: 'checking', + }); + }); + }); + + const transactionsRaw = [ + { + transactionId: '000010348081381', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-4.00', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 63053 51590342815 KAUFUMSATZ 24.90 2311825 ARN044873748454374484719431 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '085179a2e5fa34b0ff71b3f2c9f4876f', + date: '2023-12-29', + }, + { + transactionId: '000010348081380', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-2.00', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 8987 90671935362 KAUFUMSATZ 94.81 929614 ARN54795476045598005130492 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '0707bbe2de27e5aabfd5dc614c584951', + date: '2023-12-29', + }, + { + transactionId: '000010348081379', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-6.00', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 2206 17679024325 KAUFUMSATZ 55.25 819456 ARN08595270353806495555431 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '4b15b590652c9ebdc3f974591b15b250', + date: '2023-12-29', + }, + { + transactionId: '000010348081378', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-12.99', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 9437 535-182-825 LU KAUFUMSATZ 43.79 665448 ARN86236748928277201384604 ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: 'f930f8c153f3e37fb9906e4b3a2b4552', + date: '2023-12-29', + }, + { + transactionId: '000010348081377', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-9.00', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 3582 98236826123 KAUFUMSATZ 88.90 477561 ARN64452564252952225664357 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '1ce866282deb78cc4ff4cd108e11b8cc', + date: '2023-12-29', + }, + { + transactionId: '000010347374680', + endToEndId: '9212020-0900000070-2023121711315956', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '2892.61', + currency: 'EUR', + }, + debtorName: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:F22685813 Gehalt 80/6586', + proprietaryBankTransactionCode: 'Gehalt/Rente', + internalTransactionId: 'e731d8eb47f1ae96ccc11e1fb8b76a60', + date: '2023-12-29', + }, + { + transactionId: '000010336959253', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-28', + valueDate: '2023-12-28', + transactionAmount: { + amount: '-85.80', + currency: 'EUR', + }, + creditorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 7082 FAUCOGNEY E FR KAUFUMSATZ 38.20 265113 ARN47998616225906149245029 ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '2bbc054ae7ba299482a7849fded864f3', + date: '2023-12-28', + }, + { + transactionId: '000010350537843', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-29', + valueDate: '2023-12-27', + transactionAmount: { + amount: '2.79', + currency: 'EUR', + }, + debtorName: ' ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation: Zins/Dividende ISIN IE36B9RBWM04 VANG.FTSE', + proprietaryBankTransactionCode: 'Zins / Dividende WP', + internalTransactionId: '3bb7c58199d3fa5a44e85871d9001798', + date: '2023-12-29', + }, + { + transactionId: '000010341786083', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-28', + valueDate: '2023-12-27', + transactionAmount: { + amount: '79.80', + currency: 'EUR', + }, + debtorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 4619 GUTSCHRIFTSBELEG 03.91 134870 ', + proprietaryBankTransactionCode: 'Gutschrift', + internalTransactionId: '5570eefb7213e39153a6c7fb97d7dc6f', + date: '2023-12-28', + }, + { + transactionId: '000010328399902', + endToEndId: 'NOTPROVIDED', + bookingDate: '2023-12-27', + valueDate: '2023-12-27', + transactionAmount: { + amount: '-10.90', + currency: 'EUR', + }, + debtorName: 'VISA XXXXXXXXXXXXXXXXXXXX ', + remittanceInformationUnstructured: + 'mandatereference:,creditorid:,remittanceinformation:NR XXXX 3465 XXXXXXXXX KAUFUMSATZ 90.40 505416 ARN63639757770303957985044 Google Pay ', + proprietaryBankTransactionCode: 'Lastschrifteinzug', + internalTransactionId: '1b1bf30b23afb56ba4d41b9c65cf0efa', + date: '2023-12-27', + }, + ]; + + describe('#sortTransactions', () => { + it('handles empty arrays', () => { + const transactions = []; + const sortedTransactions = IngIngddeff.sortTransactions(transactions); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = IngIngddeff.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + + it('returns sorted array for unsorted inputs', () => { + const normalizeTransactions = transactionsRaw.map((tx) => + IngIngddeff.normalizeTransaction(tx, true), + ); + const originalOrder = Array.from(normalizeTransactions); + const swap = (a, b) => { + const swap = normalizeTransactions[a]; + normalizeTransactions[a] = normalizeTransactions[b]; + normalizeTransactions[b] = swap; + }; + swap(1, 4); + swap(3, 6); + swap(0, 7); + const sortedTransactions = IngIngddeff.sortTransactions( + normalizeTransactions, + ); + expect(sortedTransactions).toEqual(originalOrder); + }); + }); + + describe('#countStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '3596.87', currency: 'EUR' }, + balanceType: 'interimBooked', + lastChangeDateTime: '2023-12-29T16:44:06.479Z', + }, + ]; + + it('should calculate the starting balance correctly', () => { + const normalizeTransactions = transactionsRaw.map((tx) => + IngIngddeff.normalizeTransaction(tx, true), + ); + const sortedTransactions = IngIngddeff.sortTransactions( + normalizeTransactions, + ); + + const startingBalance = IngIngddeff.calculateStartingBalance( + sortedTransactions, + balances, + ); + + expect(startingBalance).toEqual(75236); + }); + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + expect( + IngIngddeff.calculateStartingBalance(transactions, balances), + ).toEqual(359687); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js new file mode 100644 index 00000000000..a38a6164667 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js @@ -0,0 +1,200 @@ +import IngPlIngbplpw from '../ing_pl_ingbplpw.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('IngPlIngbplpw', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + resourceId: 'PL00000000000000000987654321', + iban: 'PL00000000000000000987654321', + currency: 'PLN', + ownerName: 'John Example', + product: 'Current Account for Individuals (Retail)', + bic: 'INGBPLPW', + ownerAddressUnstructured: [ + 'UL. EXAMPLE STREET 10 M.1', + '00-000 WARSZAWA', + ], + id: 'd3eccc94-9536-48d3-98be-813f79199ee3', + created: '2022-07-24T20:45:47.929582Z', + last_accessed: '2023-01-24T22:12:00.193558Z', + institution_id: 'ING_PL_INGBPLPW', + status: 'READY', + owner_name: '', + institution: { + id: 'ING_PL_INGBPLPW', + name: 'ING', + bic: 'INGBPLPW', + transaction_total_days: '365', + countries: ['PL'], + logo: 'https://cdn.nordigen.com/ais/ING_PL_INGBPLPW.png', + supported_payments: {}, + supported_features: [ + 'access_scopes', + 'business_accounts', + 'card_accounts', + 'corporate_accounts', + 'pending_transactions', + 'private_accounts', + ], + }, + }; + + it('returns normalized account data returned to Frontend', () => { + const normalizedAccount = IngPlIngbplpw.normalizeAccount(accountRaw); + expect(normalizedAccount).toMatchInlineSnapshot(` + { + "account_id": "d3eccc94-9536-48d3-98be-813f79199ee3", + "iban": "PL00000000000000000987654321", + "institution": { + "bic": "INGBPLPW", + "countries": [ + "PL", + ], + "id": "ING_PL_INGBPLPW", + "logo": "https://cdn.nordigen.com/ais/ING_PL_INGBPLPW.png", + "name": "ING", + "supported_features": [ + "access_scopes", + "business_accounts", + "card_accounts", + "corporate_accounts", + "pending_transactions", + "private_accounts", + ], + "supported_payments": {}, + "transaction_total_days": "365", + }, + "mask": "4321", + "name": "Current Account for Individuals (Retail) (XXX 4321) PLN", + "official_name": "Current Account for Individuals (Retail)", + "type": "checking", + } + `); + }); + }); + + describe('#sortTransactions', () => { + it('sorts transactions by time and sequence from newest to oldest', () => { + const transactions = [ + { + transactionId: 'D202301180000003', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000004', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301230000001', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000002', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301200000001', + transactionAmount: mockTransactionAmount, + }, + ]; + const sortedTransactions = IngPlIngbplpw.sortTransactions(transactions); + expect(sortedTransactions).toEqual([ + { + transactionId: 'D202301230000001', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301200000001', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000004', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000003', + transactionAmount: mockTransactionAmount, + }, + { + transactionId: 'D202301180000002', + transactionAmount: mockTransactionAmount, + }, + ]); + }); + + it('handles empty arrays', () => { + const transactions = []; + const sortedTransactions = IngPlIngbplpw.sortTransactions(transactions); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = IngPlIngbplpw.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + }); + + describe('#countStartingBalance', () => { + it('should calculate the starting balance correctly', () => { + /** @type {import('../../gocardless-node.types.js').Transaction[]} */ + const sortedTransactions = [ + { + transactionAmount: { amount: '-100.00', currency: 'USD' }, + balanceAfterTransaction: { + balanceAmount: { amount: '400.00', currency: 'USD' }, + balanceType: 'interimBooked', + }, + }, + { + transactionAmount: { amount: '50.00', currency: 'USD' }, + balanceAfterTransaction: { + balanceAmount: { amount: '450.00', currency: 'USD' }, + balanceType: 'interimBooked', + }, + }, + { + transactionAmount: { amount: '-25.00', currency: 'USD' }, + balanceAfterTransaction: { + balanceAmount: { amount: '475.00', currency: 'USD' }, + balanceType: 'interimBooked', + }, + }, + ]; + + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceType: 'interimBooked', + balanceAmount: { amount: '500.00', currency: 'USD' }, + }, + { + balanceType: 'closingBooked', + balanceAmount: { amount: '600.00', currency: 'USD' }, + }, + ]; + + const startingBalance = IngPlIngbplpw.calculateStartingBalance( + sortedTransactions, + balances, + ); + + expect(startingBalance).toEqual(50000); + }); + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceType: 'interimBooked', + balanceAmount: { amount: '500.00', currency: 'USD' }, + }, + ]; + expect( + IngPlIngbplpw.calculateStartingBalance(transactions, balances), + ).toEqual(50000); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/integration-bank.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/integration-bank.spec.js new file mode 100644 index 00000000000..f244d00a9a3 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/integration-bank.spec.js @@ -0,0 +1,157 @@ +import { jest } from '@jest/globals'; +import IntegrationBank from '../integration-bank.js'; +import { + mockExtendAccountsAboutInstitutions, + mockInstitution, +} from '../../services/tests/fixtures.js'; + +describe('IntegrationBank', () => { + let consoleSpy; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'debug'); + }); + + describe('normalizeAccount', () => { + const account = mockExtendAccountsAboutInstitutions[0]; + + it('should return a normalized account object', () => { + const normalizedAccount = IntegrationBank.normalizeAccount(account); + expect(normalizedAccount).toEqual({ + account_id: account.id, + institution: mockInstitution, + mask: '4321', + iban: account.iban, + name: 'account-example-one (XXX 4321) PLN', + official_name: 'integration-SANDBOXFINANCE_SFIN0000', + type: 'checking', + }); + }); + + it('should return a normalized account object with masked value "0000" when no iban property is provided', () => { + const normalizedAccount = IntegrationBank.normalizeAccount({ + ...account, + iban: undefined, + }); + expect(normalizedAccount).toEqual({ + account_id: account.id, + institution: mockInstitution, + mask: '0000', + iban: null, + name: 'account-example-one PLN', + official_name: 'integration-SANDBOXFINANCE_SFIN0000', + type: 'checking', + }); + }); + + it('normalizeAccount logs available account properties', () => { + IntegrationBank.normalizeAccount(account); + expect(consoleSpy).toHaveBeenCalledWith( + 'Available account properties for new institution integration', + { + account: JSON.stringify(account), + }, + ); + }); + }); + + describe('sortTransactions', () => { + const transactions = [ + { + date: '2022-01-01', + bookingDate: '2022-01-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + date: '2022-01-03', + bookingDate: '2022-01-03', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + date: '2022-01-02', + bookingDate: '2022-01-02', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]; + const sortedTransactions = [ + { + date: '2022-01-03', + bookingDate: '2022-01-03', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + date: '2022-01-02', + bookingDate: '2022-01-02', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + date: '2022-01-01', + bookingDate: '2022-01-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]; + + it('should return transactions sorted by bookingDate', () => { + const sortedTransactions = IntegrationBank.sortTransactions(transactions); + expect(sortedTransactions).toEqual(sortedTransactions); + }); + + it('sortTransactions logs available transactions properties', () => { + IntegrationBank.sortTransactions(transactions); + expect(consoleSpy).toHaveBeenCalledWith( + 'Available (first 10) transactions properties for new integration of institution in sortTransactions function', + { top10Transactions: JSON.stringify(sortedTransactions.slice(0, 10)) }, + ); + }); + }); + + describe('calculateStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Transaction[]} */ + const transactions = [ + { + bookingDate: '2022-01-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + bookingDate: '2022-02-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + bookingDate: '2022-03-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]; + + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '1000.00', currency: 'EUR' }, + balanceType: 'interimBooked', + }, + ]; + + it('should return 0 when no transactions or balances are provided', () => { + const startingBalance = IntegrationBank.calculateStartingBalance([], []); + expect(startingBalance).toEqual(0); + }); + + it('should return 70000 when transactions and balances are provided', () => { + const startingBalance = IntegrationBank.calculateStartingBalance( + transactions, + balances, + ); + expect(startingBalance).toEqual(70000); + }); + + it('logs available transactions and balances properties', () => { + IntegrationBank.calculateStartingBalance(transactions, balances); + expect(consoleSpy).toHaveBeenCalledWith( + 'Available (first 10) transactions properties for new integration of institution in calculateStartingBalance function', + { + balances: JSON.stringify(balances), + top10SortedTransactions: JSON.stringify(transactions.slice(0, 10)), + }, + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/integration_bank.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/integration_bank.spec.js new file mode 100644 index 00000000000..a73da647cb0 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/integration_bank.spec.js @@ -0,0 +1,157 @@ +import { jest } from '@jest/globals'; +import IntegrationBank from '../integration-bank.js'; +import { + mockExtendAccountsAboutInstitutions, + mockInstitution, +} from '../../services/tests/fixtures.js'; + +describe('IntegrationBank', () => { + let consoleSpy; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'debug'); + }); + + describe('normalizeAccount', () => { + const account = mockExtendAccountsAboutInstitutions[0]; + + it('should return a normalized account object', () => { + const normalizedAccount = IntegrationBank.normalizeAccount(account); + expect(normalizedAccount).toEqual({ + account_id: account.id, + institution: mockInstitution, + mask: '4321', + iban: account.iban, + name: 'account-example-one (XXX 4321) PLN', + official_name: 'Savings Account for Individuals (Retail)', + type: 'checking', + }); + }); + + it('should return a normalized account object with masked value "0000" when no iban property is provided', () => { + const normalizedAccount = IntegrationBank.normalizeAccount({ + ...account, + iban: undefined, + }); + expect(normalizedAccount).toEqual({ + account_id: account.id, + institution: mockInstitution, + mask: '0000', + iban: null, + name: 'account-example-one PLN', + official_name: 'Savings Account for Individuals (Retail)', + type: 'checking', + }); + }); + + it('normalizeAccount logs available account properties', () => { + IntegrationBank.normalizeAccount(account); + expect(consoleSpy).toHaveBeenCalledWith( + 'Available account properties for new institution integration', + { + account: JSON.stringify(account), + }, + ); + }); + }); + + describe('sortTransactions', () => { + const transactions = [ + { + date: '2022-01-01', + bookingDate: '2022-01-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + date: '2022-01-03', + bookingDate: '2022-01-03', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + date: '2022-01-02', + bookingDate: '2022-01-02', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]; + const sortedTransactions = [ + { + date: '2022-01-03', + bookingDate: '2022-01-03', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + date: '2022-01-02', + bookingDate: '2022-01-02', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + date: '2022-01-01', + bookingDate: '2022-01-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]; + + it('should return transactions sorted by bookingDate', () => { + const sortedTransactions = IntegrationBank.sortTransactions(transactions); + expect(sortedTransactions).toEqual(sortedTransactions); + }); + + it('sortTransactions logs available transactions properties', () => { + IntegrationBank.sortTransactions(transactions); + expect(consoleSpy).toHaveBeenCalledWith( + 'Available (first 10) transactions properties for new integration of institution in sortTransactions function', + { top10Transactions: JSON.stringify(sortedTransactions.slice(0, 10)) }, + ); + }); + }); + + describe('calculateStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Transaction[]} */ + const transactions = [ + { + bookingDate: '2022-01-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + bookingDate: '2022-02-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + bookingDate: '2022-03-01', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]; + + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '1000.00', currency: 'EUR' }, + balanceType: 'interimBooked', + }, + ]; + + it('should return 0 when no transactions or balances are provided', () => { + const startingBalance = IntegrationBank.calculateStartingBalance([], []); + expect(startingBalance).toEqual(0); + }); + + it('should return 70000 when transactions and balances are provided', () => { + const startingBalance = IntegrationBank.calculateStartingBalance( + transactions, + balances, + ); + expect(startingBalance).toEqual(70000); + }); + + it('logs available transactions and balances properties', () => { + IntegrationBank.calculateStartingBalance(transactions, balances); + expect(consoleSpy).toHaveBeenCalledWith( + 'Available (first 10) transactions properties for new integration of institution in calculateStartingBalance function', + { + balances: JSON.stringify(balances), + top10SortedTransactions: JSON.stringify(transactions.slice(0, 10)), + }, + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js new file mode 100644 index 00000000000..21e67dcd36a --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js @@ -0,0 +1,36 @@ +import KBCkredbebb from '../kbc_kredbebb.js'; + +describe('kbc_kredbebb', () => { + describe('#normalizeTransaction', () => { + it('returns the remittanceInformationUnstructured as payeeName when the amount is negative', () => { + const transaction = { + remittanceInformationUnstructured: + 'CARREFOUR ST GIL BE1060 BRUXELLES Betaling met Google Pay via Debit Mastercard 28-08-2024 om 19.15 uur 5127 04XX XXXX 1637 5853 98XX XXXX 2266 JOHN SMITH', + transactionAmount: { amount: '-10.99', currency: 'EUR' }, + }; + const normalizedTransaction = KBCkredbebb.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.payeeName).toEqual( + 'CARREFOUR ST GIL BE1060 BRUXELLES', + ); + }); + + it('returns the debtorName as payeeName when the amount is positive', () => { + const transaction = { + debtorName: 'CARREFOUR ST GIL BE1060 BRUXELLES', + remittanceInformationUnstructured: + 'CARREFOUR ST GIL BE1060 BRUXELLES Betaling met Google Pay via Debit Mastercard 28-08-2024 om 19.15 uur 5127 04XX XXXX 1637 5853 98XX XXXX 2266 JOHN SMITH', + transactionAmount: { amount: '10.99', currency: 'EUR' }, + }; + const normalizedTransaction = KBCkredbebb.normalizeTransaction( + transaction, + true, + ); + expect(normalizedTransaction.payeeName).toEqual( + 'CARREFOUR ST GIL BE1060 BRUXELLES', + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/mbank-retail-brexplpw.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/mbank-retail-brexplpw.spec.js new file mode 100644 index 00000000000..d8212b26048 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/mbank-retail-brexplpw.spec.js @@ -0,0 +1,169 @@ +import MbankRetailBrexplpw from '../mbank-retail-brexplpw.js'; + +describe('MbankRetailBrexplpw', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + iban: 'PL00000000000000000987654321', + currency: 'PLN', + ownerName: 'John Example', + displayName: 'EKONTO', + product: 'RACHUNEK BIEŻĄCY', + usage: 'PRIV', + ownerAddressUnstructured: [ + 'POL', + 'UL. EXAMPLE STREET 10 M.1', + '00-000 WARSZAWA', + ], + id: 'd3eccc94-9536-48d3-98be-813f79199ee3', + created: '2023-01-18T13:24:55.879512Z', + last_accessed: null, + institution_id: 'MBANK_RETAIL_BREXPLPW', + status: 'READY', + owner_name: '', + institution: { + id: 'MBANK_RETAIL_BREXPLPW', + name: 'mBank Retail', + bic: 'BREXPLPW', + transaction_total_days: '90', + countries: ['PL'], + logo: 'https://cdn.nordigen.com/ais/MBANK_RETAIL_BREXCZPP.png', + supported_payments: {}, + supported_features: [ + 'access_scopes', + 'business_accounts', + 'card_accounts', + 'corporate_accounts', + 'pending_transactions', + 'private_accounts', + ], + }, + }; + it('returns normalized account data returned to Frontend', () => { + expect(MbankRetailBrexplpw.normalizeAccount(accountRaw)) + .toMatchInlineSnapshot(` + { + "account_id": "d3eccc94-9536-48d3-98be-813f79199ee3", + "iban": "PL00000000000000000987654321", + "institution": { + "bic": "BREXPLPW", + "countries": [ + "PL", + ], + "id": "MBANK_RETAIL_BREXPLPW", + "logo": "https://cdn.nordigen.com/ais/MBANK_RETAIL_BREXCZPP.png", + "name": "mBank Retail", + "supported_features": [ + "access_scopes", + "business_accounts", + "card_accounts", + "corporate_accounts", + "pending_transactions", + "private_accounts", + ], + "supported_payments": {}, + "transaction_total_days": "90", + }, + "mask": "4321", + "name": "EKONTO (XXX 4321)", + "official_name": "RACHUNEK BIEŻĄCY", + "type": "checking", + } + `); + }); + }); + + describe('#sortTransactions', () => { + it('returns transactions from newest to oldest', () => { + const sortedTransactions = MbankRetailBrexplpw.sortTransactions([ + { + transactionId: '202212300001', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300003', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300002', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300000', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202112300001', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]); + + expect(sortedTransactions).toEqual([ + { + transactionId: '202212300003', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300002', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300001', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300000', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202112300001', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]); + }); + + it('returns empty array for empty input', () => { + const sortedTransactions = MbankRetailBrexplpw.sortTransactions([]); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = + MbankRetailBrexplpw.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + }); + + describe('#countStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '1000.00', currency: 'PLN' }, + balanceType: 'interimBooked', + }, + ]; + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + expect( + MbankRetailBrexplpw.calculateStartingBalance(transactions, balances), + ).toEqual(100000); + }); + + it('returns the balance minus the available transactions', () => { + const transactions = [ + { + transactionAmount: { amount: '200.00', currency: 'PLN' }, + }, + { + transactionAmount: { amount: '300.50', currency: 'PLN' }, + }, + ]; + + expect( + MbankRetailBrexplpw.calculateStartingBalance(transactions, balances), + ).toEqual(49950); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js new file mode 100644 index 00000000000..d4113df8ffa --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js @@ -0,0 +1,169 @@ +import MbankRetailBrexplpw from '../mbank_retail_brexplpw.js'; + +describe('MbankRetailBrexplpw', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + iban: 'PL00000000000000000987654321', + currency: 'PLN', + ownerName: 'John Example', + displayName: 'EKONTO', + product: 'RACHUNEK BIEŻĄCY', + usage: 'PRIV', + ownerAddressUnstructured: [ + 'POL', + 'UL. EXAMPLE STREET 10 M.1', + '00-000 WARSZAWA', + ], + id: 'd3eccc94-9536-48d3-98be-813f79199ee3', + created: '2023-01-18T13:24:55.879512Z', + last_accessed: null, + institution_id: 'MBANK_RETAIL_BREXPLPW', + status: 'READY', + owner_name: '', + institution: { + id: 'MBANK_RETAIL_BREXPLPW', + name: 'mBank Retail', + bic: 'BREXPLPW', + transaction_total_days: '90', + countries: ['PL'], + logo: 'https://cdn.nordigen.com/ais/MBANK_RETAIL_BREXCZPP.png', + supported_payments: {}, + supported_features: [ + 'access_scopes', + 'business_accounts', + 'card_accounts', + 'corporate_accounts', + 'pending_transactions', + 'private_accounts', + ], + }, + }; + it('returns normalized account data returned to Frontend', () => { + expect(MbankRetailBrexplpw.normalizeAccount(accountRaw)) + .toMatchInlineSnapshot(` + { + "account_id": "d3eccc94-9536-48d3-98be-813f79199ee3", + "iban": "PL00000000000000000987654321", + "institution": { + "bic": "BREXPLPW", + "countries": [ + "PL", + ], + "id": "MBANK_RETAIL_BREXPLPW", + "logo": "https://cdn.nordigen.com/ais/MBANK_RETAIL_BREXCZPP.png", + "name": "mBank Retail", + "supported_features": [ + "access_scopes", + "business_accounts", + "card_accounts", + "corporate_accounts", + "pending_transactions", + "private_accounts", + ], + "supported_payments": {}, + "transaction_total_days": "90", + }, + "mask": "4321", + "name": "EKONTO (XXX 4321) PLN", + "official_name": "RACHUNEK BIEŻĄCY", + "type": "checking", + } + `); + }); + }); + + describe('#sortTransactions', () => { + it('returns transactions from newest to oldest', () => { + const sortedTransactions = MbankRetailBrexplpw.sortTransactions([ + { + transactionId: '202212300001', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300003', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300002', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300000', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202112300001', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]); + + expect(sortedTransactions).toEqual([ + { + transactionId: '202212300003', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300002', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300001', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202212300000', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + { + transactionId: '202112300001', + transactionAmount: { amount: '100', currency: 'EUR' }, + }, + ]); + }); + + it('returns empty array for empty input', () => { + const sortedTransactions = MbankRetailBrexplpw.sortTransactions([]); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = + MbankRetailBrexplpw.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + }); + + describe('#countStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '1000.00', currency: 'PLN' }, + balanceType: 'interimBooked', + }, + ]; + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + expect( + MbankRetailBrexplpw.calculateStartingBalance(transactions, balances), + ).toEqual(100000); + }); + + it('returns the balance minus the available transactions', () => { + const transactions = [ + { + transactionAmount: { amount: '200.00', currency: 'PLN' }, + }, + { + transactionAmount: { amount: '300.50', currency: 'PLN' }, + }, + ]; + + expect( + MbankRetailBrexplpw.calculateStartingBalance(transactions, balances), + ).toEqual(49950); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/nationwide-naiagb21.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/nationwide-naiagb21.spec.js new file mode 100644 index 00000000000..5dc549528b7 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/nationwide-naiagb21.spec.js @@ -0,0 +1,105 @@ +import Nationwide from '../nationwide-naiagb21.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('Nationwide', () => { + describe('#normalizeTransaction', () => { + it('retains date for booked transaction', () => { + const d = new Date(); + d.setDate(d.getDate() - 7); + + const date = d.toISOString().split('T')[0]; + + const transaction = { + bookingDate: date, + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.date).toEqual(date); + }); + + it('fixes date for pending transactions', () => { + const d = new Date(); + const date = d.toISOString().split('T')[0]; + + const transaction = { + bookingDate: date, + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(new Date(normalizedTransaction.date).getTime()).toBeLessThan( + d.getTime(), + ); + }); + + it('keeps transactionId if in the correct format', () => { + const transactionId = 'a896729bb8b30b5ca862fe70bd5967185e2b5d3a'; + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + transactionId, + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.transactionId).toBe(transactionId); + }); + + it('unsets transactionId if not valid length', () => { + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + transactionId: '0123456789', + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.transactionId).toBeNull(); + }); + + it('unsets transactionId if debit placeholder found', () => { + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + transactionId: '00DEBIT202401010000000000-1000SUPERMARKET', + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.transactionId).toBeNull(); + }); + + it('unsets transactionId if credit placeholder found', () => { + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + transactionId: '00CREDIT202401010000000000-1000SUPERMARKET', + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.transactionId).toBeNull(); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js new file mode 100644 index 00000000000..9e95c85f41d --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js @@ -0,0 +1,105 @@ +import Nationwide from '../nationwide_naiagb21.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('Nationwide', () => { + describe('#normalizeTransaction', () => { + it('retains date for booked transaction', () => { + const d = new Date(); + d.setDate(d.getDate() - 7); + + const date = d.toISOString().split('T')[0]; + + const transaction = { + bookingDate: date, + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.date).toEqual(date); + }); + + it('fixes date for pending transactions', () => { + const d = new Date(); + const date = d.toISOString().split('T')[0]; + + const transaction = { + bookingDate: date, + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(new Date(normalizedTransaction.date).getTime()).toBeLessThan( + d.getTime(), + ); + }); + + it('keeps transactionId if in the correct format', () => { + const transactionId = 'a896729bb8b30b5ca862fe70bd5967185e2b5d3a'; + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + transactionId, + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.transactionId).toBe(transactionId); + }); + + it('unsets transactionId if not valid length', () => { + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + transactionId: '0123456789', + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.transactionId).toBeNull(); + }); + + it('unsets transactionId if debit placeholder found', () => { + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + transactionId: '00DEBIT202401010000000000-1000SUPERMARKET', + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.transactionId).toBeNull(); + }); + + it('unsets transactionId if credit placeholder found', () => { + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + transactionId: '00CREDIT202401010000000000-1000SUPERMARKET', + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Nationwide.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.transactionId).toBeNull(); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js new file mode 100644 index 00000000000..990c6cd5e2c --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js @@ -0,0 +1,48 @@ +import NbgEthngraaxxx from '../nbg_ethngraaxxx.js'; + +describe('NbgEthngraaxxx', () => { + describe('#normalizeTransaction', () => { + it('provides correct amount in pending transaction and removes payee prefix', () => { + const transaction = { + bookingDate: '2024-09-03', + date: '2024-09-03', + remittanceInformationUnstructured: 'ΑΓΟΡΑ testingson', + transactionAmount: { + amount: '100.00', + currency: 'EUR', + }, + valueDate: '2024-09-03', + }; + + const normalizedTransaction = NbgEthngraaxxx.normalizeTransaction( + transaction, + false, + ); + + expect(normalizedTransaction.transactionAmount.amount).toEqual('-100.00'); + expect(normalizedTransaction.payeeName).toEqual('Testingson'); + }); + }); + + it('provides correct amount and payee in booked transaction', () => { + const transaction = { + transactionId: 'O244015L68IK', + bookingDate: '2024-09-03', + date: '2024-09-03', + remittanceInformationUnstructured: 'testingson', + transactionAmount: { + amount: '-100.00', + currency: 'EUR', + }, + valueDate: '2024-09-03', + }; + + const normalizedTransaction = NbgEthngraaxxx.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.transactionAmount.amount).toEqual('-100.00'); + expect(normalizedTransaction.payeeName).toEqual('Testingson'); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js new file mode 100644 index 00000000000..40e8bef752d --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js @@ -0,0 +1,46 @@ +import RevolutRevolt21 from '../revolut_revolt21.js'; + +describe('RevolutRevolt21', () => { + describe('#normalizeTransaction', () => { + it('returns the expected remittanceInformationUnstructured from a bizum expense transfer', () => { + const transaction = { + transactionAmount: { amount: '-1.00', currency: 'EUR' }, + remittanceInformationUnstructuredArray: [ + 'Bizum payment to: CREDITOR NAME', + 'Bizum description', + ], + bookingDate: '2024-09-21', + }; + + const normalizedTransaction = RevolutRevolt21.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + 'Bizum description', + ); + }); + }); + + it('returns the expected payeeName and remittanceInformationUnstructured from a bizum income transfer', () => { + const transaction = { + transactionAmount: { amount: '1.00', currency: 'EUR' }, + remittanceInformationUnstructuredArray: [ + 'Bizum payment from: DEBTOR NAME', + 'Bizum description', + ], + bookingDate: '2024-09-21', + }; + + const normalizedTransaction = RevolutRevolt21.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.payeeName).toEqual('DEBTOR NAME'); + expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + 'Bizum description', + ); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/sandboxfinance-sfin0000.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/sandboxfinance-sfin0000.spec.js new file mode 100644 index 00000000000..ba705f7e738 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/sandboxfinance-sfin0000.spec.js @@ -0,0 +1,131 @@ +import SandboxfinanceSfin0000 from '../sandboxfinance-sfin0000.js'; + +describe('SandboxfinanceSfin0000', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + resourceId: '01F3NS5ASCNMVCTEJDT0G215YE', + iban: 'GL0865354374424724', + currency: 'EUR', + ownerName: 'Jane Doe', + name: 'Main Account', + product: 'Checkings', + cashAccountType: 'CACC', + id: '99a0bfe2-0bef-46df-bff2-e9ae0c6c5838', + created: '2022-02-21T13:43:55.608911Z', + last_accessed: '2023-01-25T16:50:15.078264Z', + institution_id: 'SANDBOXFINANCE_SFIN0000', + status: 'READY', + owner_name: 'Jane Doe', + institution: { + id: 'SANDBOXFINANCE_SFIN0000', + name: 'Sandbox Finance', + bic: 'SFIN0000', + transaction_total_days: '90', + countries: ['XX'], + logo: 'https://cdn.nordigen.com/ais/SANDBOXFINANCE_SFIN0000.png', + supported_payments: {}, + supported_features: [], + }, + }; + + it('returns normalized account data returned to Frontend', () => { + expect(SandboxfinanceSfin0000.normalizeAccount(accountRaw)) + .toMatchInlineSnapshot(` + { + "account_id": "99a0bfe2-0bef-46df-bff2-e9ae0c6c5838", + "iban": "GL0865354374424724", + "institution": { + "bic": "SFIN0000", + "countries": [ + "XX", + ], + "id": "SANDBOXFINANCE_SFIN0000", + "logo": "https://cdn.nordigen.com/ais/SANDBOXFINANCE_SFIN0000.png", + "name": "Sandbox Finance", + "supported_features": [], + "supported_payments": {}, + "transaction_total_days": "90", + }, + "mask": "4724", + "name": "Main Account (XXX 4724)", + "official_name": "Checkings", + "type": "checking", + } + `); + }); + }); + + describe('#sortTransactions', () => { + it('handles empty arrays', () => { + const transactions = []; + const sortedTransactions = + SandboxfinanceSfin0000.sortTransactions(transactions); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = + SandboxfinanceSfin0000.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + }); + + describe('#countStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '1000.00', currency: 'PLN' }, + balanceType: 'interimAvailable', + }, + ]; + + it('should calculate the starting balance correctly', () => { + const sortedTransactions = [ + { + transactionId: '2022-01-01-1', + transactionAmount: { amount: '-100.00', currency: 'USD' }, + }, + { + transactionId: '2022-01-01-2', + transactionAmount: { amount: '50.00', currency: 'USD' }, + }, + { + transactionId: '2022-01-01-3', + transactionAmount: { amount: '-25.00', currency: 'USD' }, + }, + ]; + + const startingBalance = SandboxfinanceSfin0000.calculateStartingBalance( + sortedTransactions, + balances, + ); + + expect(startingBalance).toEqual(107500); + }); + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + expect( + SandboxfinanceSfin0000.calculateStartingBalance(transactions, balances), + ).toEqual(100000); + }); + + it('returns the balance minus the available transactions', () => { + /** @type {import('../../gocardless-node.types.js').Transaction[]} */ + const transactions = [ + { + transactionAmount: { amount: '200.00', currency: 'PLN' }, + }, + { + transactionAmount: { amount: '300.50', currency: 'PLN' }, + }, + ]; + + expect( + SandboxfinanceSfin0000.calculateStartingBalance(transactions, balances), + ).toEqual(49950); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js new file mode 100644 index 00000000000..a0ac23c1f19 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js @@ -0,0 +1,131 @@ +import SandboxfinanceSfin0000 from '../sandboxfinance_sfin0000.js'; + +describe('SandboxfinanceSfin0000', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + resourceId: '01F3NS5ASCNMVCTEJDT0G215YE', + iban: 'GL0865354374424724', + currency: 'EUR', + ownerName: 'Jane Doe', + name: 'Main Account', + product: 'Checkings', + cashAccountType: 'CACC', + id: '99a0bfe2-0bef-46df-bff2-e9ae0c6c5838', + created: '2022-02-21T13:43:55.608911Z', + last_accessed: '2023-01-25T16:50:15.078264Z', + institution_id: 'SANDBOXFINANCE_SFIN0000', + status: 'READY', + owner_name: 'Jane Doe', + institution: { + id: 'SANDBOXFINANCE_SFIN0000', + name: 'Sandbox Finance', + bic: 'SFIN0000', + transaction_total_days: '90', + countries: ['XX'], + logo: 'https://cdn.nordigen.com/ais/SANDBOXFINANCE_SFIN0000.png', + supported_payments: {}, + supported_features: [], + }, + }; + + it('returns normalized account data returned to Frontend', () => { + expect(SandboxfinanceSfin0000.normalizeAccount(accountRaw)) + .toMatchInlineSnapshot(` + { + "account_id": "99a0bfe2-0bef-46df-bff2-e9ae0c6c5838", + "iban": "GL0865354374424724", + "institution": { + "bic": "SFIN0000", + "countries": [ + "XX", + ], + "id": "SANDBOXFINANCE_SFIN0000", + "logo": "https://cdn.nordigen.com/ais/SANDBOXFINANCE_SFIN0000.png", + "name": "Sandbox Finance", + "supported_features": [], + "supported_payments": {}, + "transaction_total_days": "90", + }, + "mask": "4724", + "name": "Main Account (XXX 4724) EUR", + "official_name": "Checkings", + "type": "checking", + } + `); + }); + }); + + describe('#sortTransactions', () => { + it('handles empty arrays', () => { + const transactions = []; + const sortedTransactions = + SandboxfinanceSfin0000.sortTransactions(transactions); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = + SandboxfinanceSfin0000.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + }); + + describe('#countStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '1000.00', currency: 'PLN' }, + balanceType: 'interimAvailable', + }, + ]; + + it('should calculate the starting balance correctly', () => { + const sortedTransactions = [ + { + transactionId: '2022-01-01-1', + transactionAmount: { amount: '-100.00', currency: 'USD' }, + }, + { + transactionId: '2022-01-01-2', + transactionAmount: { amount: '50.00', currency: 'USD' }, + }, + { + transactionId: '2022-01-01-3', + transactionAmount: { amount: '-25.00', currency: 'USD' }, + }, + ]; + + const startingBalance = SandboxfinanceSfin0000.calculateStartingBalance( + sortedTransactions, + balances, + ); + + expect(startingBalance).toEqual(107500); + }); + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + expect( + SandboxfinanceSfin0000.calculateStartingBalance(transactions, balances), + ).toEqual(100000); + }); + + it('returns the balance minus the available transactions', () => { + /** @type {import('../../gocardless-node.types.js').Transaction[]} */ + const transactions = [ + { + transactionAmount: { amount: '200.00', currency: 'PLN' }, + }, + { + transactionAmount: { amount: '300.50', currency: 'PLN' }, + }, + ]; + + expect( + SandboxfinanceSfin0000.calculateStartingBalance(transactions, balances), + ).toEqual(49950); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/spk-marburg-biedenkopf-heladef1mar.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/spk-marburg-biedenkopf-heladef1mar.spec.js new file mode 100644 index 00000000000..5641400b217 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/spk-marburg-biedenkopf-heladef1mar.spec.js @@ -0,0 +1,254 @@ +import SpkMarburgBiedenkopfHeladef1mar from '../spk-marburg-biedenkopf-heladef1mar.js'; + +describe('SpkMarburgBiedenkopfHeladef1mar', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + resourceId: 'e896eec6-6096-4efc-a941-756bd9d74765', + iban: 'DE50533500000123456789', + currency: 'EUR', + ownerName: 'JANE DOE', + product: 'Sichteinlagen', + bic: 'HELADEF1MAR', + usage: 'PRIV', + id: 'a787ba27-02ee-4fd6-be86-73831adc5498', + created: '2024-01-01T14:17:11.630352Z', + last_accessed: '2024-01-01T14:19:42.709478Z', + institution_id: 'SPK_MARBURG_BIEDENKOPF_HELADEF1MAR', + status: 'READY', + owner_name: 'JANE DOE', + institution: { + id: 'SPK_MARBURG_BIEDENKOPF_HELADEF1MAR', + name: 'Sparkasse Marburg-Biedenkopf', + bic: 'HELADEF1MAR', + transaction_total_days: '360', + countries: ['DE'], + logo: 'https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/sparkasse.png', + supported_payments: { + 'single-payment': ['SCT', 'ISCT'], + }, + supported_features: [ + 'card_accounts', + 'payments', + 'pending_transactions', + ], + /*"identification_codes": []*/ + }, + }; + + it('returns normalized account data returned to Frontend', () => { + expect( + SpkMarburgBiedenkopfHeladef1mar.normalizeAccount(accountRaw), + ).toEqual({ + account_id: 'a787ba27-02ee-4fd6-be86-73831adc5498', + iban: 'DE50533500000123456789', + institution: { + bic: 'HELADEF1MAR', + countries: ['DE'], + id: 'SPK_MARBURG_BIEDENKOPF_HELADEF1MAR', + logo: 'https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/sparkasse.png', + name: 'Sparkasse Marburg-Biedenkopf', + supported_features: [ + 'card_accounts', + 'payments', + 'pending_transactions', + ], + supported_payments: { + 'single-payment': ['SCT', 'ISCT'], + }, + transaction_total_days: '360', + }, + mask: '6789', + name: 'Sichteinlagen (XXX 6789) EUR', + official_name: 'Sichteinlagen', + type: 'checking', + }); + }); + }); + + const transactionsRaw = [ + { + transactionId: 'fefa0b605ac14a7eb14f4c8ab6a6af55', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-40.00', + currency: 'EUR', + }, + creditorName: 'JET Tankstelle', + remittanceInformationStructured: 'AUTORISATION 28.12. 18:30', + proprietaryBankTransactionCode: 'NSTO+000+0000+000-AA', + internalTransactionId: '761660c052ed48e78c2be39775f08da9', + date: '2023-12-29', + }, + { + transactionId: '1a8e5d0df259472694f13132001af0a6', + bookingDate: '2023-12-28', + valueDate: '2023-12-28', + transactionAmount: { + amount: '-1242.47', + currency: 'EUR', + }, + creditorName: 'Peter Muster', + remittanceInformationStructured: 'Miete 12/2023', + proprietaryBankTransactionCode: 'NSTO+111+1111+111-BB', + internalTransactionId: '5a20ac78b146401e940b6fee30ee404b', + date: '2023-12-28', + }, + { + transactionId: '166983e65ec54000a361a952e6161f33', + bookingDate: '2023-12-27', + valueDate: '2023-12-27', + transactionAmount: { + amount: '1541.23', + currency: 'EUR', + }, + debtorName: 'Arbeitgeber AG', + remittanceInformationStructured: 'Lohn/Gehalt 12/2023', + proprietaryBankTransactionCode: 'NSTO+222+2222+222-CC', + internalTransactionId: '51630dda877f45f186d315b8058d891a', + date: '2023-12-27', + }, + { + transactionId: '4dd9f4c9968a45739c0705ebc675b54b', + bookingDate: '2023-12-26', + valueDate: '2023-12-26', + transactionAmount: { + amount: '-8.00', + currency: 'EUR', + }, + remittanceInformationStructuredArray: [ + 'Entgeltabrechnung', + 'siehe Anlage', + ], + proprietaryBankTransactionCode: 'NSTO+333+3333+333-DD', + internalTransactionId: '9c58c87c2d1644e4a5e149c837c16bbb', + date: '2023-12-26', + }, + ]; + + describe('#normalizeTransaction', () => { + it('fallbacks to remittanceInformationStructured when remittanceInformationUnstructed is not set', () => { + const transaction = { + transactionId: 'fefa0b605ac14a7eb14f4c8ab6a6af55', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-40.00', + currency: 'EUR', + }, + creditorName: 'JET Tankstelle', + remittanceInformationStructured: 'AUTORISATION 28.12. 18:30', + proprietaryBankTransactionCode: 'NSTO+000+0000+000-AA', + internalTransactionId: '761660c052ed48e78c2be39775f08da9', + date: '2023-12-29', + }; + + expect( + SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(transaction, true) + .remittanceInformationUnstructured, + ).toEqual('AUTORISATION 28.12. 18:30'); + }); + + it('fallbacks to remittanceInformationStructuredArray when remittanceInformationUnstructed and remittanceInformationStructured is not set', () => { + const transaction = { + transactionId: '4dd9f4c9968a45739c0705ebc675b54b', + bookingDate: '2023-12-26', + valueDate: '2023-12-26', + transactionAmount: { + amount: '-8.00', + currency: 'EUR', + }, + remittanceInformationStructuredArray: [ + 'Entgeltabrechnung', + 'siehe Anlage', + ], + proprietaryBankTransactionCode: 'NSTO+333+3333+333-DD', + internalTransactionId: '9c58c87c2d1644e4a5e149c837c16bbb', + date: '2023-12-26', + }; + + expect( + SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(transaction, true) + .remittanceInformationUnstructured, + ).toEqual('Entgeltabrechnung siehe Anlage'); + }); + }); + + describe('#sortTransactions', () => { + it('handles empty arrays', () => { + const transactions = []; + const sortedTransactions = + SpkMarburgBiedenkopfHeladef1mar.sortTransactions(transactions); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = + SpkMarburgBiedenkopfHeladef1mar.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + + it('returns sorted array for unsorted inputs', () => { + const normalizeTransactions = transactionsRaw.map((tx) => + SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(tx, true), + ); + const originalOrder = Array.from(normalizeTransactions); + const swap = (a, b) => { + const swap = normalizeTransactions[a]; + normalizeTransactions[a] = normalizeTransactions[b]; + normalizeTransactions[b] = swap; + }; + swap(1, 4); + swap(3, 6); + swap(0, 7); + const sortedTransactions = + SpkMarburgBiedenkopfHeladef1mar.sortTransactions(normalizeTransactions); + expect(sortedTransactions).toEqual(originalOrder); + }); + }); + + describe('#countStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '3596.87', currency: 'EUR' }, + balanceType: 'closingBooked', + referenceDate: '2023-12-29', + }, + ]; + + it('should return 0 when no transactions or balances are provided', () => { + const startingBalance = + SpkMarburgBiedenkopfHeladef1mar.calculateStartingBalance([], []); + expect(startingBalance).toEqual(0); + }); + + it('should calculate the starting balance correctly', () => { + const normalizeTransactions = transactionsRaw.map((tx) => + SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(tx, true), + ); + const sortedTransactions = + SpkMarburgBiedenkopfHeladef1mar.sortTransactions(normalizeTransactions); + + const startingBalance = + SpkMarburgBiedenkopfHeladef1mar.calculateStartingBalance( + sortedTransactions, + balances, + ); + + expect(startingBalance).toEqual(334611); + }); + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + expect( + SpkMarburgBiedenkopfHeladef1mar.calculateStartingBalance( + transactions, + balances, + ), + ).toEqual(359687); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js new file mode 100644 index 00000000000..bf3bf365b00 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js @@ -0,0 +1,254 @@ +import SpkMarburgBiedenkopfHeladef1mar from '../spk_marburg_biedenkopf_heladef1mar.js'; + +describe('SpkMarburgBiedenkopfHeladef1mar', () => { + describe('#normalizeAccount', () => { + /** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution} */ + const accountRaw = { + resourceId: 'e896eec6-6096-4efc-a941-756bd9d74765', + iban: 'DE50533500000123456789', + currency: 'EUR', + ownerName: 'JANE DOE', + product: 'Sichteinlagen', + bic: 'HELADEF1MAR', + usage: 'PRIV', + id: 'a787ba27-02ee-4fd6-be86-73831adc5498', + created: '2024-01-01T14:17:11.630352Z', + last_accessed: '2024-01-01T14:19:42.709478Z', + institution_id: 'SPK_MARBURG_BIEDENKOPF_HELADEF1MAR', + status: 'READY', + owner_name: 'JANE DOE', + institution: { + id: 'SPK_MARBURG_BIEDENKOPF_HELADEF1MAR', + name: 'Sparkasse Marburg-Biedenkopf', + bic: 'HELADEF1MAR', + transaction_total_days: '360', + countries: ['DE'], + logo: 'https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/sparkasse.png', + supported_payments: { + 'single-payment': ['SCT', 'ISCT'], + }, + supported_features: [ + 'card_accounts', + 'payments', + 'pending_transactions', + ], + /*"identification_codes": []*/ + }, + }; + + it('returns normalized account data returned to Frontend', () => { + expect( + SpkMarburgBiedenkopfHeladef1mar.normalizeAccount(accountRaw), + ).toEqual({ + account_id: 'a787ba27-02ee-4fd6-be86-73831adc5498', + iban: 'DE50533500000123456789', + institution: { + bic: 'HELADEF1MAR', + countries: ['DE'], + id: 'SPK_MARBURG_BIEDENKOPF_HELADEF1MAR', + logo: 'https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/sparkasse.png', + name: 'Sparkasse Marburg-Biedenkopf', + supported_features: [ + 'card_accounts', + 'payments', + 'pending_transactions', + ], + supported_payments: { + 'single-payment': ['SCT', 'ISCT'], + }, + transaction_total_days: '360', + }, + mask: '6789', + name: 'Sichteinlagen (XXX 6789) EUR', + official_name: 'Sichteinlagen', + type: 'checking', + }); + }); + }); + + const transactionsRaw = [ + { + transactionId: 'fefa0b605ac14a7eb14f4c8ab6a6af55', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-40.00', + currency: 'EUR', + }, + creditorName: 'JET Tankstelle', + remittanceInformationStructured: 'AUTORISATION 28.12. 18:30', + proprietaryBankTransactionCode: 'NSTO+000+0000+000-AA', + internalTransactionId: '761660c052ed48e78c2be39775f08da9', + date: '2023-12-29', + }, + { + transactionId: '1a8e5d0df259472694f13132001af0a6', + bookingDate: '2023-12-28', + valueDate: '2023-12-28', + transactionAmount: { + amount: '-1242.47', + currency: 'EUR', + }, + creditorName: 'Peter Muster', + remittanceInformationStructured: 'Miete 12/2023', + proprietaryBankTransactionCode: 'NSTO+111+1111+111-BB', + internalTransactionId: '5a20ac78b146401e940b6fee30ee404b', + date: '2023-12-28', + }, + { + transactionId: '166983e65ec54000a361a952e6161f33', + bookingDate: '2023-12-27', + valueDate: '2023-12-27', + transactionAmount: { + amount: '1541.23', + currency: 'EUR', + }, + debtorName: 'Arbeitgeber AG', + remittanceInformationStructured: 'Lohn/Gehalt 12/2023', + proprietaryBankTransactionCode: 'NSTO+222+2222+222-CC', + internalTransactionId: '51630dda877f45f186d315b8058d891a', + date: '2023-12-27', + }, + { + transactionId: '4dd9f4c9968a45739c0705ebc675b54b', + bookingDate: '2023-12-26', + valueDate: '2023-12-26', + transactionAmount: { + amount: '-8.00', + currency: 'EUR', + }, + remittanceInformationStructuredArray: [ + 'Entgeltabrechnung', + 'siehe Anlage', + ], + proprietaryBankTransactionCode: 'NSTO+333+3333+333-DD', + internalTransactionId: '9c58c87c2d1644e4a5e149c837c16bbb', + date: '2023-12-26', + }, + ]; + + describe('#normalizeTransaction', () => { + it('fallbacks to remittanceInformationStructured when remittanceInformationUnstructed is not set', () => { + const transaction = { + transactionId: 'fefa0b605ac14a7eb14f4c8ab6a6af55', + bookingDate: '2023-12-29', + valueDate: '2023-12-29', + transactionAmount: { + amount: '-40.00', + currency: 'EUR', + }, + creditorName: 'JET Tankstelle', + remittanceInformationStructured: 'AUTORISATION 28.12. 18:30', + proprietaryBankTransactionCode: 'NSTO+000+0000+000-AA', + internalTransactionId: '761660c052ed48e78c2be39775f08da9', + date: '2023-12-29', + }; + + expect( + SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(transaction, true) + .remittanceInformationUnstructured, + ).toEqual('AUTORISATION 28.12. 18:30'); + }); + + it('fallbacks to remittanceInformationStructuredArray when remittanceInformationUnstructed and remittanceInformationStructured is not set', () => { + const transaction = { + transactionId: '4dd9f4c9968a45739c0705ebc675b54b', + bookingDate: '2023-12-26', + valueDate: '2023-12-26', + transactionAmount: { + amount: '-8.00', + currency: 'EUR', + }, + remittanceInformationStructuredArray: [ + 'Entgeltabrechnung', + 'siehe Anlage', + ], + proprietaryBankTransactionCode: 'NSTO+333+3333+333-DD', + internalTransactionId: '9c58c87c2d1644e4a5e149c837c16bbb', + date: '2023-12-26', + }; + + expect( + SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(transaction, true) + .remittanceInformationUnstructured, + ).toEqual('Entgeltabrechnung siehe Anlage'); + }); + }); + + describe('#sortTransactions', () => { + it('handles empty arrays', () => { + const transactions = []; + const sortedTransactions = + SpkMarburgBiedenkopfHeladef1mar.sortTransactions(transactions); + expect(sortedTransactions).toEqual([]); + }); + + it('returns empty array for undefined input', () => { + const sortedTransactions = + SpkMarburgBiedenkopfHeladef1mar.sortTransactions(undefined); + expect(sortedTransactions).toEqual([]); + }); + + it('returns sorted array for unsorted inputs', () => { + const normalizeTransactions = transactionsRaw.map((tx) => + SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(tx, true), + ); + const originalOrder = Array.from(normalizeTransactions); + const swap = (a, b) => { + const swap = normalizeTransactions[a]; + normalizeTransactions[a] = normalizeTransactions[b]; + normalizeTransactions[b] = swap; + }; + swap(1, 4); + swap(3, 6); + swap(0, 7); + const sortedTransactions = + SpkMarburgBiedenkopfHeladef1mar.sortTransactions(normalizeTransactions); + expect(sortedTransactions).toEqual(originalOrder); + }); + }); + + describe('#countStartingBalance', () => { + /** @type {import('../../gocardless-node.types.js').Balance[]} */ + const balances = [ + { + balanceAmount: { amount: '3596.87', currency: 'EUR' }, + balanceType: 'closingBooked', + referenceDate: '2023-12-29', + }, + ]; + + it('should return 0 when no transactions or balances are provided', () => { + const startingBalance = + SpkMarburgBiedenkopfHeladef1mar.calculateStartingBalance([], []); + expect(startingBalance).toEqual(0); + }); + + it('should calculate the starting balance correctly', () => { + const normalizeTransactions = transactionsRaw.map((tx) => + SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(tx, true), + ); + const sortedTransactions = + SpkMarburgBiedenkopfHeladef1mar.sortTransactions(normalizeTransactions); + + const startingBalance = + SpkMarburgBiedenkopfHeladef1mar.calculateStartingBalance( + sortedTransactions, + balances, + ); + + expect(startingBalance).toEqual(334611); + }); + + it('returns the same balance amount when no transactions', () => { + const transactions = []; + + expect( + SpkMarburgBiedenkopfHeladef1mar.calculateStartingBalance( + transactions, + balances, + ), + ).toEqual(359687); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/swedbank-habalv22.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/swedbank-habalv22.spec.js new file mode 100644 index 00000000000..91e2e01217b --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/swedbank-habalv22.spec.js @@ -0,0 +1,62 @@ +import SwedbankHabaLV22 from '../swedbank-habalv22.js'; + +describe('#normalizeTransaction', () => { + const bookedCardTransaction = { + transactionId: '2024102900000000-1', + bookingDate: '2024-10-29', + valueDate: '2024-10-29', + transactionAmount: { + amount: '-22.99', + currency: 'EUR', + }, + creditorName: 'SOME CREDITOR NAME', + remittanceInformationUnstructured: + 'PIRKUMS 424242******4242 28.10.2024 22.99 EUR (111111) SOME CREDITOR NAME', + bankTransactionCode: 'PMNT-CCRD-POSD', + internalTransactionId: 'fa000f86afb2cc7678bcff0000000000', + }; + + it('extracts card transaction date', () => { + expect( + SwedbankHabaLV22.normalizeTransaction(bookedCardTransaction, true) + .bookingDate, + ).toEqual('2024-10-28'); + + expect( + SwedbankHabaLV22.normalizeTransaction(bookedCardTransaction, true).date, + ).toEqual('2024-10-28'); + }); + + it.each([ + ['regular text', 'Some info'], + ['partial card text', 'PIRKUMS xxx'], + ['null value', null], + ])('normalizes non-card transaction with %s', (_, remittanceInfo) => { + const transaction = { + ...bookedCardTransaction, + remittanceInformationUnstructured: remittanceInfo, + }; + const normalized = SwedbankHabaLV22.normalizeTransaction(transaction, true); + + expect(normalized.bookingDate).toEqual('2024-10-29'); + expect(normalized.date).toEqual('2024-10-29'); + }); + + const pendingCardTransaction = { + transactionId: '2024102900000000-1', + valueDate: '2024-10-29', + transactionAmount: { + amount: '-22.99', + currency: 'EUR', + }, + remittanceInformationUnstructured: + 'PIRKUMS 424242******4242 28.10.24 13:37 22.99 EUR (111111) SOME CREDITOR NAME', + }; + + it('extracts pending card transaction creditor name', () => { + expect( + SwedbankHabaLV22.normalizeTransaction(pendingCardTransaction, false) + .creditorName, + ).toEqual('SOME CREDITOR NAME'); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js new file mode 100644 index 00000000000..31673a4eb33 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js @@ -0,0 +1,62 @@ +import SwedbankHabaLV22 from '../swedbank_habalv22.js'; + +describe('#normalizeTransaction', () => { + const bookedCardTransaction = { + transactionId: '2024102900000000-1', + bookingDate: '2024-10-29', + valueDate: '2024-10-29', + transactionAmount: { + amount: '-22.99', + currency: 'EUR', + }, + creditorName: 'SOME CREDITOR NAME', + remittanceInformationUnstructured: + 'PIRKUMS 424242******4242 28.10.2024 22.99 EUR (111111) SOME CREDITOR NAME', + bankTransactionCode: 'PMNT-CCRD-POSD', + internalTransactionId: 'fa000f86afb2cc7678bcff0000000000', + }; + + it('extracts card transaction date', () => { + expect( + SwedbankHabaLV22.normalizeTransaction(bookedCardTransaction, true) + .bookingDate, + ).toEqual('2024-10-28'); + + expect( + SwedbankHabaLV22.normalizeTransaction(bookedCardTransaction, true).date, + ).toEqual('2024-10-28'); + }); + + it.each([ + ['regular text', 'Some info'], + ['partial card text', 'PIRKUMS xxx'], + ['null value', null], + ])('normalizes non-card transaction with %s', (_, remittanceInfo) => { + const transaction = { + ...bookedCardTransaction, + remittanceInformationUnstructured: remittanceInfo, + }; + const normalized = SwedbankHabaLV22.normalizeTransaction(transaction, true); + + expect(normalized.bookingDate).toEqual('2024-10-29'); + expect(normalized.date).toEqual('2024-10-29'); + }); + + const pendingCardTransaction = { + transactionId: '2024102900000000-1', + valueDate: '2024-10-29', + transactionAmount: { + amount: '-22.99', + currency: 'EUR', + }, + remittanceInformationUnstructured: + 'PIRKUMS 424242******4242 28.10.24 13:37 22.99 EUR (111111) SOME CREDITOR NAME', + }; + + it('extracts pending card transaction creditor name', () => { + expect( + SwedbankHabaLV22.normalizeTransaction(pendingCardTransaction, false) + .creditorName, + ).toEqual('SOME CREDITOR NAME'); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js new file mode 100644 index 00000000000..c6cb6b26cc3 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js @@ -0,0 +1,65 @@ +import Virgin from '../virgin_nrnbgb22.js'; +import { mockTransactionAmount } from '../../services/tests/fixtures.js'; + +describe('Virgin', () => { + describe('#normalizeTransaction', () => { + it('does not alter simple payee information', () => { + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + remittanceInformationUnstructured: 'DIRECT DEBIT PAYMENT', + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Virgin.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.creditorName).toEqual( + 'DIRECT DEBIT PAYMENT', + ); + expect(normalizedTransaction.debtorName).toEqual('DIRECT DEBIT PAYMENT'); + expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + 'DIRECT DEBIT PAYMENT', + ); + }); + + it('formats bank transfer payee and references', () => { + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + remittanceInformationUnstructured: 'FPS, Joe Bloggs, Food', + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Virgin.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.creditorName).toEqual('Joe Bloggs'); + expect(normalizedTransaction.debtorName).toEqual('Joe Bloggs'); + expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + 'Food', + ); + }); + + it('removes method information from payee name', () => { + const transaction = { + bookingDate: '2024-01-01T00:00:00Z', + remittanceInformationUnstructured: 'Card 99, Tesco Express', + transactionAmount: mockTransactionAmount, + }; + + const normalizedTransaction = Virgin.normalizeTransaction( + transaction, + true, + ); + + expect(normalizedTransaction.creditorName).toEqual('Tesco Express'); + expect(normalizedTransaction.debtorName).toEqual('Tesco Express'); + expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + 'Card 99, Tesco Express', + ); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js b/packages/sync-server/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js new file mode 100644 index 00000000000..4dcb7fa2dd2 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js @@ -0,0 +1,36 @@ +/** +/** + * Extracts the payee name from the unstructured remittance information string based on pattern detection. + * + * This function scans the `remittanceInformationUnstructured` string for the presence of + * any of the specified patterns and removes the substring from the position of the last + * occurrence of the most relevant pattern. If no patterns are found, it returns the original string. + * + * @param {string} [remittanceInformationUnstructured=''] - The unstructured remittance information from which to extract the payee name. + * @param {string[]} [patterns=[]] - An array of patterns to look for within the remittance information. + * These patterns are used to identify and remove unwanted parts of the remittance information. + * @returns {string} - The extracted payee name, cleaned of any matched patterns, or the original + * remittance information if no patterns are found. + * + * @example + * const remittanceInfo = 'John Doe Paiement Maestro par Carte de débit CBC 05-09-2024 à 15.43 heures 6703 19XX XXXX X...'; + * const patterns = ['Paiement', 'Domiciliation', 'Transfert', 'Ordre permanent']; + * const payeeName = extractPayeeNameFromRemittanceInfo(remittanceInfo, patterns); // --> 'John Doe' + */ +export function extractPayeeNameFromRemittanceInfo( + remittanceInformationUnstructured, + patterns, +) { + if (!remittanceInformationUnstructured || !patterns.length) { + return remittanceInformationUnstructured; + } + + const indexForRemoval = patterns.reduce((maxIndex, pattern) => { + const index = remittanceInformationUnstructured.lastIndexOf(pattern); + return index > maxIndex ? index : maxIndex; + }, -1); + + return indexForRemoval > -1 + ? remittanceInformationUnstructured.substring(0, indexForRemoval).trim() + : remittanceInformationUnstructured; +} diff --git a/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js b/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js new file mode 100644 index 00000000000..78418bc2dfe --- /dev/null +++ b/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js @@ -0,0 +1,37 @@ +import Fallback from './integration-bank.js'; + +/** @type {import('./bank.interface.js').IBank} */ +export default { + ...Fallback, + + institutionIds: ['VIRGIN_NRNBGB22'], + + normalizeTransaction(transaction, booked) { + const transferPrefixes = ['MOB', 'FPS']; + const methodRegex = /^(Card|WLT)\s\d+/; + + const parts = transaction.remittanceInformationUnstructured.split(', '); + + if (transferPrefixes.includes(parts[0])) { + // Transfer remittance information begins with either "MOB" or "FPS" + // the second field contains the payee and the third contains the + // reference + + transaction.creditorName = parts[1]; + transaction.debtorName = parts[1]; + transaction.remittanceInformationUnstructured = parts[2]; + } else if (parts[0].match(methodRegex)) { + // The payee is prefixed with the payment method, eg "Card 11, {payee}" + + transaction.creditorName = parts[1]; + transaction.debtorName = parts[1]; + } else { + // Simple payee name + + transaction.creditorName = transaction.remittanceInformationUnstructured; + transaction.debtorName = transaction.remittanceInformationUnstructured; + } + + return Fallback.normalizeTransaction(transaction, booked); + }, +}; diff --git a/packages/sync-server/src/app-gocardless/errors.js b/packages/sync-server/src/app-gocardless/errors.js new file mode 100644 index 00000000000..3a77360bfa0 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/errors.js @@ -0,0 +1,84 @@ +export class RequisitionNotLinked extends Error { + constructor(params = {}) { + super('Requisition not linked yet'); + this.details = params; + } +} + +export class AccountNotLinkedToRequisition extends Error { + constructor(accountId, requisitionId) { + super('Provided account id is not linked to given requisition'); + this.details = { + accountId, + requisitionId, + }; + } +} + +export class GenericGoCardlessError extends Error { + constructor(data = {}) { + super('GoCardless returned error'); + this.details = data; + } +} + +export class GoCardlessClientError extends Error { + constructor(message, details) { + super(message); + this.details = details; + } +} + +export class InvalidInputDataError extends GoCardlessClientError { + constructor(response) { + super('Invalid provided parameters', response); + } +} + +export class InvalidGoCardlessTokenError extends GoCardlessClientError { + constructor(response) { + super('Token is invalid or expired', response); + } +} + +export class AccessDeniedError extends GoCardlessClientError { + constructor(response) { + super('IP address access denied', response); + } +} + +export class NotFoundError extends GoCardlessClientError { + constructor(response) { + super('Resource not found', response); + } +} + +export class ResourceSuspended extends GoCardlessClientError { + constructor(response) { + super( + 'Resource was suspended due to numerous errors that occurred while accessing it', + response, + ); + } +} + +export class RateLimitError extends GoCardlessClientError { + constructor(response) { + super( + 'Daily request limit set by the Institution has been exceeded', + response, + ); + } +} + +export class UnknownError extends GoCardlessClientError { + constructor(response) { + super('Request to Institution returned an error', response); + } +} + +export class ServiceError extends GoCardlessClientError { + constructor(response) { + super('Institution service unavailable', response); + } +} diff --git a/packages/sync-server/src/app-gocardless/gocardless-node.types.ts b/packages/sync-server/src/app-gocardless/gocardless-node.types.ts new file mode 100644 index 00000000000..539c91e4c59 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/gocardless-node.types.ts @@ -0,0 +1,487 @@ +type RequisitionStatus = + | 'CR' + | 'ID' + | 'LN' + | 'RJ' + | 'ER' + | 'SU' + | 'EX' + | 'GC' + | 'UA' + | 'GA' + | 'SA'; + +export type Requisition = { + /** + * option to enable account selection view for the end user + */ + account_selection: boolean; + + /** + * array of account IDs retrieved within a scope of this requisition + */ + accounts: string[]; + + /** + * EUA associated with this requisition + */ + agreement: string; + + /** + * The date & time at which the requisition was created. + */ + created: string; + + /** + * The unique ID of the requisition + */ + id: string; + + /** + * an Institution ID for this Requisition + */ + institution_id: string; + + /** + * link to initiate authorization with Institution + */ + link: string; + + /** + * redirect URL to your application after end-user authorization with ASPSP + */ + redirect: string; + + /** + * enable redirect back to the client after account list received + */ + redirect_immediate: boolean; + + /** + * additional ID to identify the end user + */ + reference: string; + + /** + * optional SSN field to verify ownership of the account + */ + ssn: string; + + /** + * status of this requisition + */ + status: RequisitionStatus; + + /** + * A two-letter country code (ISO 639-1) + */ + user_language: string; +}; + +/** + * Object representing GoCardless account details + * Account details will be returned in Berlin Group PSD2 format. + */ +export type GoCardlessAccountDetails = { + /** + * Resource id of the account + */ + resourceId?: string; + + /** + * BBAN of the account. This data element is used for payment accounts which have no IBAN + */ + bban?: string; + + /** + * BIC associated to the account + */ + bic?: string; + + /** + * External Cash Account Type 1 Code from ISO 20022 + */ + cashAccountType?: string; + + /** + * Currency of the account + */ + currency: string; + + /** + * Specifications that might be provided by the financial institution, including + * - Characteristics of the account + * - Characteristics of the relevant card + */ + details?: string; + + /** + * Name of the account as defined by the end user within online channels + */ + displayName?: string; + + /** + * IBAN of the account + */ + iban?: string; + + /** + * This data attribute is a field where a financial institution can name a cash account associated with pending card transactions + */ + linkedAccounts?: string; + + /** + * Alias to a payment account via a registered mobile phone number + */ + msisdn?: string; + + /** + * Name of the account, as assigned by the financial institution + */ + name?: string; + + /** + * Address of the legal account owner + */ + ownerAddressUnstructured?: string[]; + + /** + * Name of the legal account owner. If there is more than one owner, then two names might be noted here. For a corporate account, the corporate name is used for this attribute. + */ + ownerName?: string; + + /** + * Product Name of the Bank for this account, proprietary definition + */ + product?: string; + + /** + * Account status. The value is one of the following: + * - "enabled": account is available + * - "deleted": account is terminated + * - "blocked": account is blocked, e.g. for legal reasons + * + * If this field is not used, then the account is considered available according to the specification. + */ + status?: 'enabled' | 'deleted' | 'blocked'; + + /** + * Specifies the usage of the account: + * - PRIV: private personal account + * - ORGA: professional account + */ + usage?: 'PRIV' | 'ORGA'; +}; + +/** + * Representation of the GoCardless account metadata + */ +export type GoCardlessAccountMetadata = { + /** + * ID of the GoCardless account metadata + */ + id: string; + /** + * Date when the GoCardless account metadata was created + */ + created: string; + /** + * Date of the last access to the GoCardless account metadata + */ + last_accessed: string; + /** + * IBAN of the GoCardless account metadata + */ + iban: string; + /** + * ID of the institution associated with the GoCardless account metadata + */ + institution_id: string; + /** + * Status of the GoCardless account + * DISCOVERED: User has successfully authenticated and account is discovered + * PROCESSING: Account is being processed by the Institution + * ERROR: An error was encountered when processing account + * EXPIRED: Access to account has expired as set in End User Agreement + * READY: Account has been successfully processed + * SUSPENDED: Account has been suspended (more than 10 consecutive failed attempts to access the account) + */ + status: + | 'DISCOVERED' + | 'PROCESSING' + | 'ERROR' + | 'EXPIRED' + | 'READY' + | 'SUSPENDED'; + /** + * Name of the owner of the GoCardless account metadata + */ + owner_name: string; +}; + +/** + * Information about the Institution + */ +export type Institution = { + /** + * The id of the institution, for example "N26_NTSBDEB1" + */ + id: string; + + /** + * The name of the institution, for example "N26 Bank" + */ + name: string; + + /** + * The BIC of the institution, for example "NTSBDEB1" + */ + bic: string; + + /** + * The total number of days of transactions available, for example "90" + */ + transaction_total_days: string; + + /** + * The countries where the institution operates, for example `["PL"]` + */ + countries: string[]; + + /** + * The logo URL of the institution, for example "https://cdn.nordigen.com/ais/N26_SANDBOX_NTSBDEB1.png" + */ + logo: string; + + supported_payments?: object; + supported_features?: string[]; +}; + +/** + * An object containing information about a balance + */ +export type Balance = { + /** + * An object containing the balance amount and currency + */ + balanceAmount: Amount; + /** + * The type of balance + */ + balanceType: + | 'closingBooked' + | 'expected' + | 'forwardAvailable' + | 'interimAvailable' + | 'interimBooked' + | 'nonInvoiced' + | 'openingBooked'; + /** + * A flag indicating if the credit limit of the corresponding account is included in the calculation of the balance (if applicable) + */ + creditLimitIncluded?: boolean; + /** + * The date and time of the last change to the balance + */ + lastChangeDateTime?: string; + /** + * The reference of the last committed transaction to support the TPP in identifying whether all end users transactions are already known + */ + lastCommittedTransaction?: string; + /** + * The date of the balance + */ + referenceDate?: string; +}; + +/** + * An object representing the amount of a transaction + */ +export type Amount = { + /** + * The amount of the transaction + */ + amount: string; + + /** + * The currency of the transaction + */ + currency: string; +}; + +/** + * An object representing a financial transaction + */ +export type Transaction = { + /** + * Might be used by the financial institution to transport additional transaction-related information. + */ + additionalInformation?: string; + + /** + * Is used if and only if the bookingStatus entry equals "information". + */ + bookingStatus?: string; + + /** + * The balance after this transaction. Recommended balance type is interimBooked. + */ + balanceAfterTransaction?: Pick<Balance, 'balanceType' | 'balanceAmount'>; + + /** + * Bank transaction code as used by the financial institution and using the sub elements of this structured code defined by ISO20022. For standing order reports the following codes are applicable: + * "PMNT-ICDT-STDO" for credit transfers, + * "PMNT-IRCT-STDO" for instant credit transfers, + * "PMNT-ICDT-XBST" for cross-border credit transfers, + * "PMNT-IRCT-XBST" for cross-border real-time credit transfers, + * "PMNT-MCOP-OTHR" for specific standing orders which have a dynamic amount to move left funds e.g. on month end to a saving account + */ + bankTransactionCode?: string; + + /** + * The date when an entry is posted to an account on the financial institution's books. + */ + bookingDate?: string; + + /** + * The date and time when an entry is posted to an account on the financial institution's books. + */ + bookingDateTime?: string; + + /** + * Identification of a cheque + */ + checkId?: string; + + /** + * Account reference, conditional + */ + creditorAccount?: string; + + /** + * BICFI + */ + creditorAgent?: string; + + /** + * Identification of creditors, e.g. a SEPA Creditor ID + */ + creditorId?: string; + + /** + * Name of the creditor if a "debited" transaction + */ + creditorName?: string; + + /** + * Array of report exchange rates + */ + currencyExchange?: string[]; + + /** + * Account reference, conditional + */ + debtorAccount?: { + iban: string; + }; + + /** + * BICFI + */ + debtorAgent?: string; + + /** + * Name of the debtor if a "credited" transaction + */ + debtorName?: string; + + /** + * Unique end-to-end ID + */ + endToEndId?: string; + + /** + * The identification of the transaction as used for reference given by the financial institution. + */ + entryReference?: string; + + /** + * Transaction identifier given by GoCardless + */ + internalTransactionId?: string; + + /** + * Identification of Mandates, e.g. a SEPA Mandate ID + */ + mandateId?: string; + + /** + * Merchant category code as defined by card issuer + */ + merchantCategoryCode?: string; + + /** + * Proprietary bank transaction code as used within a community or within an financial institution + */ + proprietaryBankTransactionCode?: string; + + /** + * Conditional + */ + purposeCode?: string; + + /** + * Reference as contained in the structured remittance reference structure + */ + remittanceInformationStructured?: string; + + /** + * Reference as contained in the structured remittance array reference structure + */ + remittanceInformationStructuredArray?: string[]; + + /** + * Reference as contained in the unstructured remittance reference structure + */ + remittanceInformationUnstructured?: string; + + /** + * Reference as contained in the unstructured remittance array reference structure + */ + remittanceInformationUnstructuredArray?: string[]; + + /** + * The amount of the transaction as billed to the account + */ + transactionAmount: Amount; + + /** + * Unique transaction identifier given by financial institution + */ + transactionId?: string; + + /** + * + */ + ultimateCreditor?: string; + + /** + * + */ + ultimateDebtor?: string; + + /** + * The Date at which assets become available to the account owner in case of a credit + */ + valueDate?: string; + + /** + * The date and time at which assets become available to the account owner in case of a credit + */ + valueDateTime?: string; +}; + +export type Transactions = { + booked: Transaction[]; + pending: Transaction[]; +}; diff --git a/packages/sync-server/src/app-gocardless/gocardless.types.ts b/packages/sync-server/src/app-gocardless/gocardless.types.ts new file mode 100644 index 00000000000..373749d915d --- /dev/null +++ b/packages/sync-server/src/app-gocardless/gocardless.types.ts @@ -0,0 +1,93 @@ +import { + GoCardlessAccountMetadata, + GoCardlessAccountDetails, + Institution, + Transactions, + Balance, + Transaction, +} from './gocardless-node.types.js'; + +export type DetailedAccount = Omit<GoCardlessAccountDetails, 'status'> & + GoCardlessAccountMetadata; +export type DetailedAccountWithInstitution = DetailedAccount & { + institution: Institution; +}; +export type TransactionWithBookedStatus = Transaction & { booked: boolean }; + +export type NormalizedAccountDetails = { + /** + * Id of the account + */ + account_id: string; + + /** + * Institution of account + */ + institution: Institution; + + /** + * last 4 digits from the account iban + */ + mask: string; + + /** + * the account iban + */ + iban: string; + + /** + * Name displayed on the UI of Actual app + */ + name: string; + + /** + * name of the product in the institution + */ + official_name: string; + + /** + * type of account + */ + type: string; +}; + +export type GetTransactionsParams = { + /** + * Id of the institution from GoCardless + */ + institutionId: string; + + /** + * Id of account from the GoCardless app + */ + accountId: string; + + /** + * Begin date of the period from which we want to download transactions + */ + startDate: string; + + /** + * End date of the period from which we want to download transactions + */ + endDate?: string; +}; + +export type GetTransactionsResponse = { + status_code?: number; + detail?: string; + transactions: Transactions; +}; + +export type CreateRequisitionParams = { + institutionId: string; + + /** + * Host of your frontend app - on this host you will be redirected after linking with bank + */ + host: string; +}; + +export type GetBalances = { + balances: Balance[]; +}; diff --git a/packages/sync-server/src/app-gocardless/link.html b/packages/sync-server/src/app-gocardless/link.html new file mode 100644 index 00000000000..cd3155849ed --- /dev/null +++ b/packages/sync-server/src/app-gocardless/link.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <title>Actual + + + + +

Please wait...

+

+ The window should close automatically. If nothing happened you can close + this window or tab. +

+ + diff --git a/packages/sync-server/src/app-gocardless/services/gocardless-service.js b/packages/sync-server/src/app-gocardless/services/gocardless-service.js new file mode 100644 index 00000000000..754e36d1183 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/services/gocardless-service.js @@ -0,0 +1,621 @@ +import BankFactory, { BANKS_WITH_LIMITED_HISTORY } from '../bank-factory.js'; +import { + AccessDeniedError, + AccountNotLinkedToRequisition, + GenericGoCardlessError, + InvalidInputDataError, + InvalidGoCardlessTokenError, + NotFoundError, + RateLimitError, + ResourceSuspended, + RequisitionNotLinked, + ServiceError, + UnknownError, +} from '../errors.js'; +import * as nordigenNode from 'nordigen-node'; +import * as uuid from 'uuid'; +import jwt from 'jws'; +import { SecretName, secretsService } from '../../services/secrets-service.js'; + +const GoCardlessClient = nordigenNode.default; + +const clients = new Map(); + +const getGocardlessClient = () => { + const secrets = { + secretId: secretsService.get(SecretName.gocardless_secretId), + secretKey: secretsService.get(SecretName.gocardless_secretKey), + }; + + const hash = JSON.stringify(secrets); + + if (!clients.has(hash)) { + clients.set(hash, new GoCardlessClient(secrets)); + } + + return clients.get(hash); +}; + +export const handleGoCardlessError = (error) => { + const status = error?.response?.status; + + switch (status) { + case 400: + throw new InvalidInputDataError(error); + case 401: + throw new InvalidGoCardlessTokenError(error); + case 403: + throw new AccessDeniedError(error); + case 404: + throw new NotFoundError(error); + case 409: + throw new ResourceSuspended(error); + case 429: + throw new RateLimitError(error); + case 500: + throw new UnknownError(error); + case 503: + throw new ServiceError(error); + default: + throw new GenericGoCardlessError(error); + } +}; + +export const goCardlessService = { + /** + * Check if the GoCardless service is configured to be used. + * @returns {boolean} + */ + isConfigured: () => { + return !!( + getGocardlessClient().secretId && getGocardlessClient().secretKey + ); + }, + + /** + * + * @returns {Promise} + */ + setToken: async () => { + const isExpiredJwtToken = (token) => { + const decodedToken = jwt.decode(token); + if (!decodedToken) { + return true; + } + const payload = decodedToken.payload; + const clockTimestamp = Math.floor(Date.now() / 1000); + return clockTimestamp >= payload.exp; + }; + + if (isExpiredJwtToken(getGocardlessClient().token)) { + // Generate new access token. Token is valid for 24 hours + // Note: access_token is automatically injected to other requests after you successfully obtain it + try { + await client.generateToken(); + } catch (error) { + handleGoCardlessError(error); + } + } + }, + + /** + * + * @param requisitionId + * @throws {RequisitionNotLinked} Will throw an error if requisition is not in Linked + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise} + */ + getLinkedRequisition: async (requisitionId) => { + const requisition = await goCardlessService.getRequisition(requisitionId); + + const { status } = requisition; + + // Continue only if status of requisition is "LN" what does + // mean that account has been successfully linked to requisition + if (status !== 'LN') { + throw new RequisitionNotLinked({ requisitionStatus: status }); + } + + return requisition; + }, + + /** + * Returns requisition and all linked accounts in their Bank format. + * Each account object is extended about details of the institution + * @param requisitionId + * @throws {RequisitionNotLinked} Will throw an error if requisition is not in Linked + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise<{requisition: import('../gocardless-node.types.js').Requisition, accounts: Array}>} + */ + getRequisitionWithAccounts: async (requisitionId) => { + const requisition = await goCardlessService.getLinkedRequisition( + requisitionId, + ); + + let institutionIdSet = new Set(); + const detailedAccounts = await Promise.all( + requisition.accounts.map(async (accountId) => { + const account = await goCardlessService.getDetailedAccount(accountId); + institutionIdSet.add(account.institution_id); + return account; + }), + ); + + const institutions = await Promise.all( + Array.from(institutionIdSet).map(async (institutionId) => { + return await goCardlessService.getInstitution(institutionId); + }), + ); + + const extendedAccounts = + await goCardlessService.extendAccountsAboutInstitutions({ + accounts: detailedAccounts, + institutions, + }); + + const normalizedAccounts = extendedAccounts.map((account) => { + const bankAccount = BankFactory(account.institution_id); + return bankAccount.normalizeAccount(account); + }); + + return { requisition, accounts: normalizedAccounts }; + }, + + /** + * + * @param requisitionId + * @param accountId + * @param startDate + * @param endDate + * @throws {AccountNotLinkedToRequisition} Will throw an error if requisition not includes provided account id + * @throws {RequisitionNotLinked} Will throw an error if requisition is not in Linked + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise<{balances: Array, institutionId: string, transactions: {booked: Array, pending: Array, all: Array}, startingBalance: number}>} + */ + getTransactionsWithBalance: async ( + requisitionId, + accountId, + startDate, + endDate, + ) => { + const { institution_id, accounts: accountIds } = + await goCardlessService.getLinkedRequisition(requisitionId); + + if (!accountIds.includes(accountId)) { + throw new AccountNotLinkedToRequisition(accountId, requisitionId); + } + + const [normalizedTransactions, accountBalance] = await Promise.all([ + goCardlessService.getNormalizedTransactions( + requisitionId, + accountId, + startDate, + endDate, + ), + goCardlessService.getBalances(accountId), + ]); + + const transactions = normalizedTransactions.transactions; + + const bank = BankFactory(institution_id); + + const startingBalance = bank.calculateStartingBalance( + transactions.booked, + accountBalance.balances, + ); + + return { + balances: accountBalance.balances, + institutionId: institution_id, + startingBalance, + transactions, + }; + }, + + /** + * + * @param requisitionId + * @param accountId + * @param startDate + * @param endDate + * @throws {AccountNotLinkedToRequisition} Will throw an error if requisition not includes provided account id + * @throws {RequisitionNotLinked} Will throw an error if requisition is not in Linked + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise<{institutionId: string, transactions: {booked: Array, pending: Array, all: Array}}>} + */ + getNormalizedTransactions: async ( + requisitionId, + accountId, + startDate, + endDate, + ) => { + const { institution_id, accounts: accountIds } = + await goCardlessService.getLinkedRequisition(requisitionId); + + if (!accountIds.includes(accountId)) { + throw new AccountNotLinkedToRequisition(accountId, requisitionId); + } + + const transactions = await goCardlessService.getTransactions({ + institutionId: institution_id, + accountId, + startDate, + endDate, + }); + + const bank = BankFactory(institution_id); + const sortedBookedTransactions = bank.sortTransactions( + transactions.transactions?.booked, + ); + const sortedPendingTransactions = bank.sortTransactions( + transactions.transactions?.pending, + ); + const allTransactions = sortedBookedTransactions.map((t) => { + return { ...t, booked: true }; + }); + sortedPendingTransactions.forEach((t) => + allTransactions.push({ ...t, booked: false }), + ); + const sortedAllTransactions = bank.sortTransactions(allTransactions); + + return { + institutionId: institution_id, + transactions: { + booked: sortedBookedTransactions, + pending: sortedPendingTransactions, + all: sortedAllTransactions, + }, + }; + }, + + /** + * + * @param {import('../gocardless.types.js').CreateRequisitionParams} params + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise<{requisitionId, link}>} + */ + createRequisition: async ({ institutionId, host }) => { + await goCardlessService.setToken(); + + const institution = await goCardlessService.getInstitution(institutionId); + const bank = BankFactory(institutionId); + + let response; + try { + response = await client.initSession({ + redirectUrl: host + '/gocardless/link', + institutionId, + referenceId: uuid.v4(), + accessValidForDays: bank.accessValidForDays, + maxHistoricalDays: BANKS_WITH_LIMITED_HISTORY.includes(institutionId) + ? Number(institution.transaction_total_days) >= 90 + ? '89' + : institution.transaction_total_days + : institution.transaction_total_days, + userLanguage: 'en', + ssn: null, + redirectImmediate: false, + accountSelection: false, + }); + } catch (error) { + handleGoCardlessError(error); + } + + const { link, id: requisitionId } = response; + + return { + link, + requisitionId, + }; + }, + + /** + * Deletes requisition by provided ID + * @param requisitionId + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise<{summary: string, detail: string}>} + */ + deleteRequisition: async (requisitionId) => { + await goCardlessService.getRequisition(requisitionId); + + let response; + try { + response = client.deleteRequisition(requisitionId); + } catch (error) { + handleGoCardlessError(error); + } + + return response; + }, + + /** + * Retrieve a requisition by ID + * https://nordigen.com/en/docs/account-information/integration/parameters-and-responses/#/requisitions/requisition%20by%20id + * @param { string } requisitionId + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns { Promise } + */ + getRequisition: async (requisitionId) => { + await goCardlessService.setToken(); + + let response; + try { + response = client.getRequisitionById(requisitionId); + } catch (error) { + handleGoCardlessError(error); + } + + return response; + }, + + /** + * Retrieve an detailed account by account id + * @param accountId + * @returns {Promise} + */ + getDetailedAccount: async (accountId) => { + let detailedAccount, metadataAccount; + try { + [detailedAccount, metadataAccount] = await Promise.all([ + client.getDetails(accountId), + client.getMetadata(accountId), + ]); + } catch (error) { + handleGoCardlessError(error); + } + + return { + ...detailedAccount.account, + ...metadataAccount, + }; + }, + + /** + * Retrieve account metadata by account id + * + * Unlike getDetailedAccount, this method is not affected by institution rate-limits. + * + * @param accountId + * @returns {Promise} + */ + getAccountMetadata: async (accountId) => { + let response; + try { + response = await client.getMetadata(accountId); + } catch (error) { + handleGoCardlessError(error); + } + + return response; + }, + + /** + * Retrieve details about all Institutions in a specific country + * @param country + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise>} + */ + getInstitutions: async (country) => { + let response; + try { + response = await client.getInstitutions(country); + } catch (error) { + handleGoCardlessError(error); + } + + return response; + }, + + /** + * Retrieve details about a specific Institution + * @param institutionId + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise} + */ + getInstitution: async (institutionId) => { + let response; + try { + response = await client.getInstitutionById(institutionId); + } catch (error) { + handleGoCardlessError(error); + } + + return response; + }, + + /** + * Extends provided accounts about details of their institution + * @param {{accounts: Array, institutions: Array}} params + * @returns {Promise>} + */ + extendAccountsAboutInstitutions: async ({ accounts, institutions }) => { + const institutionsById = institutions.reduce((acc, institution) => { + acc[institution.id] = institution; + return acc; + }, {}); + + return accounts.map((account) => { + const institution = institutionsById[account.institution_id] || null; + return { + ...account, + institution, + }; + }); + }, + + /** + * Returns account transaction in provided dates + * @param {import('../gocardless.types.js').GetTransactionsParams} params + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise} + */ + getTransactions: async ({ institutionId, accountId, startDate, endDate }) => { + let response; + try { + response = await client.getTransactions({ + accountId, + dateFrom: startDate, + dateTo: endDate, + }); + } catch (error) { + handleGoCardlessError(error); + } + + const bank = BankFactory(institutionId); + response.transactions.booked = response.transactions.booked + .map((transaction) => bank.normalizeTransaction(transaction, true)) + .filter((transaction) => transaction); + response.transactions.pending = response.transactions.pending + .map((transaction) => bank.normalizeTransaction(transaction, false)) + .filter((transaction) => transaction); + + return response; + }, + + /** + * Returns account available balances + * @param accountId + * @throws {InvalidInputDataError} + * @throws {InvalidGoCardlessTokenError} + * @throws {AccessDeniedError} + * @throws {NotFoundError} + * @throws {ResourceSuspended} + * @throws {RateLimitError} + * @throws {UnknownError} + * @throws {ServiceError} + * @returns {Promise} + */ + getBalances: async (accountId) => { + let response; + try { + response = await client.getBalances(accountId); + } catch (error) { + handleGoCardlessError(error); + } + + return response; + }, +}; + +/** + * All executions of goCardlessClient should be here for testing purposes, + * as the nordigen-node library is not written in a way that is conducive to testing. + * In that way we can mock the `client` const instead of nordigen library + */ +export const client = { + getBalances: async (accountId) => + await getGocardlessClient().account(accountId).getBalances(), + getTransactions: async ({ accountId, dateFrom, dateTo }) => + await getGocardlessClient().account(accountId).getTransactions({ + dateFrom, + dateTo, + country: undefined, + }), + getInstitutions: async (country) => + await getGocardlessClient().institution.getInstitutions({ country }), + getInstitutionById: async (institutionId) => + await getGocardlessClient().institution.getInstitutionById(institutionId), + getDetails: async (accountId) => + await getGocardlessClient().account(accountId).getDetails(), + getMetadata: async (accountId) => + await getGocardlessClient().account(accountId).getMetadata(), + getRequisitionById: async (requisitionId) => + await getGocardlessClient().requisition.getRequisitionById(requisitionId), + deleteRequisition: async (requisitionId) => + await getGocardlessClient().requisition.deleteRequisition(requisitionId), + initSession: async ({ + redirectUrl, + institutionId, + referenceId, + accessValidForDays, + maxHistoricalDays, + userLanguage, + ssn, + redirectImmediate, + accountSelection, + }) => + await getGocardlessClient().initSession({ + redirectUrl, + institutionId, + referenceId, + accessValidForDays, + maxHistoricalDays, + userLanguage, + ssn, + redirectImmediate, + accountSelection, + }), + generateToken: async () => await getGocardlessClient().generateToken(), + exchangeToken: async ({ refreshToken }) => + await getGocardlessClient().exchangeToken({ refreshToken }), +}; diff --git a/packages/sync-server/src/app-gocardless/services/tests/fixtures.js b/packages/sync-server/src/app-gocardless/services/tests/fixtures.js new file mode 100644 index 00000000000..5e373e5a34d --- /dev/null +++ b/packages/sync-server/src/app-gocardless/services/tests/fixtures.js @@ -0,0 +1,180 @@ +/** @type {{balances: import('../../gocardless-node.types.js').Balance[]}} */ +export const mockedBalances = { + balances: [ + { + balanceAmount: { + amount: '657.49', + currency: 'string', + }, + balanceType: 'interimAvailable', + referenceDate: '2021-11-22', + }, + { + balanceAmount: { + amount: '185.67', + currency: 'string', + }, + balanceType: 'interimAvailable', + referenceDate: '2021-11-19', + }, + ], +}; + +/** @type {{transactions: import('../../gocardless-node.types.js').Transactions}} */ +export const mockTransactions = { + transactions: { + booked: [ + { + transactionId: 'string', + debtorName: 'string', + debtorAccount: { + iban: 'string', + }, + transactionAmount: { + currency: 'EUR', + amount: '328.18', + }, + bankTransactionCode: 'string', + bookingDate: 'date', + valueDate: 'date', + }, + { + transactionId: 'string', + transactionAmount: { + currency: 'EUR', + amount: '947.26', + }, + bankTransactionCode: 'string', + bookingDate: 'date', + valueDate: 'date', + }, + ], + pending: [ + { + transactionAmount: { + currency: 'EUR', + amount: '947.26', + }, + valueDate: 'date', + }, + ], + }, +}; + +export const mockUnknownError = { + summary: "Couldn't update account balances", + detail: 'Request to Institution returned an error', + type: 'UnknownRequestError', + status_code: 500, +}; + +/** @type {{account: import('../../gocardless-node.types.js').GoCardlessAccountDetails}} */ +export const mockAccountDetails = { + account: { + resourceId: 'PL00000000000000000987654321', + iban: 'PL00000000000000000987654321', + currency: 'PLN', + ownerName: 'JOHN EXAMPLE', + product: 'Savings Account for Individuals (Retail)', + bic: 'INGBPLPW', + ownerAddressUnstructured: ['EXAMPLE STREET 100/001', '00-000 EXAMPLE CITY'], + }, +}; + +/** @type {import('../../gocardless-node.types.js').GoCardlessAccountMetadata} */ +export const mockAccountMetaData = { + id: 'f0e49aa6-f6db-48fc-94ca-4a62372fadf4', + created: '2022-07-24T20:45:47.847062Z', + last_accessed: '2023-01-25T22:12:27.814618Z', + iban: 'PL00000000000000000987654321', + institution_id: 'SANDBOXFINANCE_SFIN0000', + status: 'READY', + owner_name: 'JOHN EXAMPLE', +}; + +/** @type {import('../../gocardless.types.js').DetailedAccount} */ +export const mockDetailedAccount = { + ...mockAccountDetails.account, + ...mockAccountMetaData, +}; + +/** @type {import('../../gocardless-node.types.js').Institution} */ +export const mockInstitution = { + id: 'N26_NTSBDEB1', + name: 'N26 Bank', + bic: 'NTSBDEB1', + transaction_total_days: '90', + countries: ['GB', 'NO', 'SE'], + logo: 'https://cdn.nordigen.com/ais/N26_SANDBOX_NTSBDEB1.png', +}; + +/** @type {import('../../gocardless-node.types.js').Requisition} */ +export const mockRequisition = { + id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + created: '2023-01-31T18:15:50.172Z', + redirect: 'string', + status: 'LN', + institution_id: 'string', + agreement: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + reference: 'string', + accounts: ['f0e49aa6-f6db-48fc-94ca-4a62372fadf4'], + user_language: 'string', + link: 'https://ob.nordigen.com/psd2/start/3fa85f64-5717-4562-b3fc-2c963f66afa6/{$INSTITUTION_ID}', + ssn: 'string', + account_selection: false, + redirect_immediate: false, +}; + +export const mockDeleteRequisition = { + summary: 'Requisition deleted', + detail: + "Requisition '$REQUISITION_ID' deleted with all its End User Agreements", +}; + +export const mockCreateRequisition = { + id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + created: '2023-02-01T15:53:29.481Z', + redirect: 'string', + status: 'CR', + institution_id: 'string', + agreement: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + reference: 'string', + accounts: [], + user_language: 'string', + link: 'https://ob.nordigen.com/psd2/start/3fa85f64-5717-4562-b3fc-2c963f66afa6/{$INSTITUTION_ID}', + ssn: 'string', + account_selection: false, + redirect_immediate: false, +}; + +/** @type {import('../../gocardless.types.js').DetailedAccount} */ +export const mockDetailedAccountExample1 = { + ...mockDetailedAccount, + name: 'account-example-one', +}; + +/** @type {import('../../gocardless.types.js').DetailedAccount} */ +export const mockDetailedAccountExample2 = { + ...mockDetailedAccount, + name: 'account-example-two', +}; + +/** @type {import('../../gocardless.types.js').DetailedAccountWithInstitution[]} */ +export const mockExtendAccountsAboutInstitutions = [ + { + ...mockDetailedAccountExample1, + institution: mockInstitution, + }, + { + ...mockDetailedAccountExample2, + institution: mockInstitution, + }, +]; + +export const mockRequisitionWithExampleAccounts = { + ...mockRequisition, + + accounts: [mockDetailedAccountExample1.id, mockDetailedAccountExample2.id], +}; + +export const mockTransactionAmount = { amount: '100', currency: 'EUR' }; diff --git a/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js b/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js new file mode 100644 index 00000000000..081fc85e044 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js @@ -0,0 +1,528 @@ +import { jest } from '@jest/globals'; +import { + AccessDeniedError, + AccountNotLinkedToRequisition, + GenericGoCardlessError, + InvalidInputDataError, + InvalidGoCardlessTokenError, + NotFoundError, + RateLimitError, + ResourceSuspended, + RequisitionNotLinked, + ServiceError, + UnknownError, +} from '../../errors.js'; + +import { + mockedBalances, + mockTransactions, + mockDetailedAccount, + mockInstitution, + mockAccountMetaData, + mockAccountDetails, + mockRequisition, + mockDeleteRequisition, + mockCreateRequisition, + mockRequisitionWithExampleAccounts, + mockDetailedAccountExample1, + mockDetailedAccountExample2, + mockExtendAccountsAboutInstitutions, +} from './fixtures.js'; + +import { + goCardlessService, + handleGoCardlessError, + client, +} from '../gocardless-service.js'; + +describe('goCardlessService', () => { + const accountId = mockAccountMetaData.id; + const requisitionId = mockRequisition.id; + + let getBalancesSpy; + let getTransactionsSpy; + let getDetailsSpy; + let getMetadataSpy; + let getInstitutionsSpy; + let getInstitutionSpy; + let getRequisitionsSpy; + let deleteRequisitionsSpy; + let createRequisitionSpy; + let setTokenSpy; + + beforeEach(() => { + getInstitutionsSpy = jest.spyOn(client, 'getInstitutions'); + getInstitutionSpy = jest.spyOn(client, 'getInstitutionById'); + getRequisitionsSpy = jest.spyOn(client, 'getRequisitionById'); + deleteRequisitionsSpy = jest.spyOn(client, 'deleteRequisition'); + createRequisitionSpy = jest.spyOn(client, 'initSession'); + getBalancesSpy = jest.spyOn(client, 'getBalances'); + getTransactionsSpy = jest.spyOn(client, 'getTransactions'); + getDetailsSpy = jest.spyOn(client, 'getDetails'); + getMetadataSpy = jest.spyOn(client, 'getMetadata'); + setTokenSpy = jest.spyOn(goCardlessService, 'setToken'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('#getLinkedRequisition', () => { + it('returns requisition', async () => { + setTokenSpy.mockResolvedValue(); + + jest + .spyOn(goCardlessService, 'getRequisition') + .mockResolvedValue(mockRequisition); + + expect( + await goCardlessService.getLinkedRequisition(requisitionId), + ).toEqual(mockRequisition); + }); + + it('throws RequisitionNotLinked error if requisition status is different than LN', async () => { + setTokenSpy.mockResolvedValue(); + + jest + .spyOn(goCardlessService, 'getRequisition') + .mockResolvedValue({ ...mockRequisition, status: 'ER' }); + + await expect(() => + goCardlessService.getLinkedRequisition(requisitionId), + ).rejects.toThrow(RequisitionNotLinked); + }); + }); + + describe('#getRequisitionWithAccounts', () => { + it('returns combined data', async () => { + jest + .spyOn(goCardlessService, 'getRequisition') + .mockResolvedValue(mockRequisitionWithExampleAccounts); + jest + .spyOn(goCardlessService, 'getDetailedAccount') + .mockResolvedValueOnce(mockDetailedAccountExample1); + jest + .spyOn(goCardlessService, 'getDetailedAccount') + .mockResolvedValueOnce(mockDetailedAccountExample2); + jest + .spyOn(goCardlessService, 'getInstitution') + .mockResolvedValue(mockInstitution); + jest + .spyOn(goCardlessService, 'extendAccountsAboutInstitutions') + .mockResolvedValue([ + { + ...mockExtendAccountsAboutInstitutions[0], + institution_id: 'NEWONE', + }, + { + ...mockExtendAccountsAboutInstitutions[1], + institution_id: 'NEWONE', + }, + ]); + + const response = await goCardlessService.getRequisitionWithAccounts( + mockRequisitionWithExampleAccounts.id, + ); + + expect(response.accounts.length).toEqual(2); + expect(response.accounts).toMatchObject( + expect.arrayContaining([ + expect.objectContaining({ + account_id: mockDetailedAccountExample1.id, + institution: mockInstitution, + official_name: 'Savings Account for Individuals (Retail)', + }), + expect.objectContaining({ + account_id: mockDetailedAccountExample2.id, + institution: mockInstitution, + official_name: 'Savings Account for Individuals (Retail)', + }), + ]), + ); + expect(response.requisition).toEqual(mockRequisitionWithExampleAccounts); + }); + }); + + describe('#getTransactionsWithBalance', () => { + const requisitionId = mockRequisition.id; + it('returns transaction with starting balance', async () => { + jest + .spyOn(goCardlessService, 'getLinkedRequisition') + .mockResolvedValue(mockRequisition); + jest + .spyOn(goCardlessService, 'getAccountMetadata') + .mockResolvedValue(mockAccountMetaData); + jest + .spyOn(goCardlessService, 'getTransactions') + .mockResolvedValue(mockTransactions); + jest + .spyOn(goCardlessService, 'getBalances') + .mockResolvedValue(mockedBalances); + + expect( + await goCardlessService.getTransactionsWithBalance( + requisitionId, + accountId, + undefined, + undefined, + ), + ).toEqual( + expect.objectContaining({ + balances: mockedBalances.balances, + institutionId: mockRequisition.institution_id, + startingBalance: expect.any(Number), + transactions: { + all: expect.arrayContaining([ + expect.objectContaining({ + bookingDate: expect.any(String), + transactionAmount: { + amount: expect.any(String), + currency: 'EUR', + }, + transactionId: expect.any(String), + valueDate: expect.any(String), + }), + expect.objectContaining({ + transactionAmount: { + amount: expect.any(String), + currency: 'EUR', + }, + valueDate: expect.any(String), + }), + ]), + booked: expect.arrayContaining([ + expect.objectContaining({ + bookingDate: expect.any(String), + transactionAmount: { + amount: expect.any(String), + currency: 'EUR', + }, + transactionId: expect.any(String), + valueDate: expect.any(String), + }), + ]), + pending: expect.arrayContaining([ + expect.objectContaining({ + transactionAmount: { + amount: expect.any(String), + currency: 'EUR', + }, + valueDate: expect.any(String), + }), + ]), + }, + }), + ); + }); + + it('throws AccountNotLinkedToRequisition error if requisition accounts not includes requested account', async () => { + jest + .spyOn(goCardlessService, 'getLinkedRequisition') + .mockResolvedValue(mockRequisition); + + await expect(() => + goCardlessService.getTransactionsWithBalance({ + requisitionId, + accountId: 'some-unknown-account-id', + startDate: undefined, + endDate: undefined, + }), + ).rejects.toThrow(AccountNotLinkedToRequisition); + }); + }); + + describe('#createRequisition', () => { + const institutionId = 'some-institution-id'; + const params = { + host: 'https://exemple.com', + institutionId, + accessValidForDays: 90, + }; + + it('calls goCardlessClient and delete requisition', async () => { + setTokenSpy.mockResolvedValue(); + getInstitutionSpy.mockResolvedValue(mockInstitution); + + createRequisitionSpy.mockResolvedValue(mockCreateRequisition); + + expect(await goCardlessService.createRequisition(params)).toEqual({ + link: expect.any(String), + requisitionId: expect.any(String), + }); + + expect(createRequisitionSpy).toBeCalledTimes(1); + }); + }); + + describe('#deleteRequisition', () => { + const requisitionId = 'some-requisition-id'; + + it('calls goCardlessClient and delete requisition', async () => { + setTokenSpy.mockResolvedValue(); + + getRequisitionsSpy.mockResolvedValue(mockRequisition); + deleteRequisitionsSpy.mockResolvedValue(mockDeleteRequisition); + + expect(await goCardlessService.deleteRequisition(requisitionId)).toEqual( + mockDeleteRequisition, + ); + + expect(getRequisitionsSpy).toBeCalledTimes(1); + expect(deleteRequisitionsSpy).toBeCalledTimes(1); + }); + }); + + describe('#getRequisition', () => { + const requisitionId = 'some-requisition-id'; + + it('calls goCardlessClient and fetch requisition', async () => { + setTokenSpy.mockResolvedValue(); + getRequisitionsSpy.mockResolvedValue(mockRequisition); + + expect(await goCardlessService.getRequisition(requisitionId)).toEqual( + mockRequisition, + ); + + expect(setTokenSpy).toBeCalledTimes(1); + expect(getRequisitionsSpy).toBeCalledTimes(1); + }); + }); + + describe('#getDetailedAccount', () => { + it('returns merged object', async () => { + getDetailsSpy.mockResolvedValue(mockAccountDetails); + getMetadataSpy.mockResolvedValue(mockAccountMetaData); + + expect(await goCardlessService.getDetailedAccount(accountId)).toEqual({ + ...mockAccountMetaData, + ...mockAccountDetails.account, + }); + expect(getDetailsSpy).toBeCalledTimes(1); + expect(getMetadataSpy).toBeCalledTimes(1); + }); + }); + + describe('#getInstitutions', () => { + const country = 'IE'; + it('calls goCardlessClient and fetch institution details', async () => { + getInstitutionsSpy.mockResolvedValue([mockInstitution]); + + expect(await goCardlessService.getInstitutions({ country })).toEqual([ + mockInstitution, + ]); + expect(getInstitutionsSpy).toBeCalledTimes(1); + }); + }); + + describe('#getInstitution', () => { + const institutionId = 'fake-institution-id'; + it('calls goCardlessClient and fetch institution details', async () => { + getInstitutionSpy.mockResolvedValue(mockInstitution); + + expect(await goCardlessService.getInstitution(institutionId)).toEqual( + mockInstitution, + ); + expect(getInstitutionSpy).toBeCalledTimes(1); + }); + }); + + describe('#extendAccountsAboutInstitutions', () => { + it('extends accounts with the corresponding institution', async () => { + const institutionA = { ...mockInstitution, id: 'INSTITUTION_A' }; + const institutionB = { ...mockInstitution, id: 'INSTITUTION_B' }; + const accountAA = { + ...mockDetailedAccount, + id: 'AA', + institution_id: 'INSTITUTION_A', + }; + const accountBB = { + ...mockDetailedAccount, + id: 'BB', + institution_id: 'INSTITUTION_B', + }; + + const accounts = [accountAA, accountBB]; + const institutions = [institutionA, institutionB]; + + const expected = [ + { + ...accountAA, + institution: institutionA, + }, + { + ...accountBB, + institution: institutionB, + }, + ]; + + const result = await goCardlessService.extendAccountsAboutInstitutions({ + accounts, + institutions, + }); + + expect(result).toEqual(expected); + }); + + it('returns accounts with missing institutions as null', async () => { + const accountAA = { + ...mockDetailedAccount, + id: 'AA', + institution_id: 'INSTITUTION_A', + }; + const accountBB = { + ...mockDetailedAccount, + id: 'BB', + institution_id: 'INSTITUTION_B', + }; + + const accounts = [accountAA, accountBB]; + + const institutionA = { ...mockInstitution, id: 'INSTITUTION_A' }; + const institutions = [institutionA]; + + const expected = [ + { + ...accountAA, + institution: institutionA, + }, + { + ...accountBB, + institution: null, + }, + ]; + + const result = await goCardlessService.extendAccountsAboutInstitutions({ + accounts, + institutions, + }); + + expect(result).toEqual(expected); + }); + }); + + describe('#getTransactions', () => { + it('calls goCardlessClient and fetch transactions for provided accountId', async () => { + getTransactionsSpy.mockResolvedValue(mockTransactions); + + expect( + await goCardlessService.getTransactions({ + institutionId: 'SANDBOXFINANCE_SFIN0000', + accountId, + startDate: '', + endDate: '', + }), + ).toMatchInlineSnapshot(` + { + "transactions": { + "booked": [ + { + "bankTransactionCode": "string", + "bookingDate": "date", + "date": "date", + "debtorAccount": { + "iban": "string", + }, + "debtorName": "string", + "payeeName": "String (stri XXX ring)", + "transactionAmount": { + "amount": "328.18", + "currency": "EUR", + }, + "transactionId": "string", + "valueDate": "date", + }, + { + "bankTransactionCode": "string", + "bookingDate": "date", + "date": "date", + "payeeName": "", + "transactionAmount": { + "amount": "947.26", + "currency": "EUR", + }, + "transactionId": "string", + "valueDate": "date", + }, + ], + "pending": [ + { + "date": "date", + "payeeName": "", + "transactionAmount": { + "amount": "947.26", + "currency": "EUR", + }, + "valueDate": "date", + }, + ], + }, + } + `); + expect(getTransactionsSpy).toBeCalledTimes(1); + }); + }); + + describe('#getBalances', () => { + it('calls goCardlessClient and fetch balances for provided accountId', async () => { + getBalancesSpy.mockResolvedValue(mockedBalances); + + expect(await goCardlessService.getBalances(accountId)).toEqual( + mockedBalances, + ); + expect(getBalancesSpy).toBeCalledTimes(1); + }); + }); +}); + +describe('#handleGoCardlessError', () => { + it('throws InvalidInputDataError for status code 400', () => { + const response = { response: { status: 400 } }; + expect(() => handleGoCardlessError(response)).toThrow( + InvalidInputDataError, + ); + }); + + it('throws InvalidGoCardlessTokenError for status code 401', () => { + const response = { response: { status: 401 } }; + expect(() => handleGoCardlessError(response)).toThrow( + InvalidGoCardlessTokenError, + ); + }); + + it('throws AccessDeniedError for status code 403', () => { + const response = { response: { status: 403 } }; + expect(() => handleGoCardlessError(response)).toThrow(AccessDeniedError); + }); + + it('throws NotFoundError for status code 404', () => { + const response = { response: { status: 404 } }; + expect(() => handleGoCardlessError(response)).toThrow(NotFoundError); + }); + + it('throws ResourceSuspended for status code 409', () => { + const response = { response: { status: 409 } }; + expect(() => handleGoCardlessError(response)).toThrow(ResourceSuspended); + }); + + it('throws RateLimitError for status code 429', () => { + const response = { response: { status: 429 } }; + expect(() => handleGoCardlessError(response)).toThrow(RateLimitError); + }); + + it('throws UnknownError for status code 500', () => { + const response = { response: { status: 500 } }; + expect(() => handleGoCardlessError(response)).toThrow(UnknownError); + }); + + it('throws ServiceError for status code 503', () => { + const response = { response: { status: 503 } }; + expect(() => handleGoCardlessError(response)).toThrow(ServiceError); + }); + + it('throws a generic error when the status code is not recognised', () => { + const response = { response: { status: 0 } }; + expect(() => handleGoCardlessError(response)).toThrow( + GenericGoCardlessError, + ); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/tests/bank-factory.spec.js b/packages/sync-server/src/app-gocardless/tests/bank-factory.spec.js new file mode 100644 index 00000000000..61dec1ddbf1 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/tests/bank-factory.spec.js @@ -0,0 +1,21 @@ +import BankFactory from '../bank-factory.js'; +import { banks } from '../bank-factory.js'; +import IntegrationBank from '../banks/integration-bank.js'; + +describe('BankFactory', () => { + it.each(banks.flatMap((bank) => bank.institutionIds))( + `should return same institutionId`, + (institutionId) => { + const result = BankFactory(institutionId); + + expect(result.institutionIds).toContain(institutionId); + }, + ); + + it('should return IntegrationBank when institutionId is not found', () => { + const institutionId = IntegrationBank.institutionIds[0]; + const result = BankFactory('fake-id-not-found'); + + expect(result.institutionIds).toContain(institutionId); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/tests/utils.spec.js b/packages/sync-server/src/app-gocardless/tests/utils.spec.js new file mode 100644 index 00000000000..9d43448d9d4 --- /dev/null +++ b/packages/sync-server/src/app-gocardless/tests/utils.spec.js @@ -0,0 +1,162 @@ +import { mockTransactionAmount } from '../services/tests/fixtures.js'; +import { sortByBookingDateOrValueDate } from '../utils.js'; + +describe('utils', () => { + describe('#sortByBookingDate', () => { + it('sorts transactions by bookingDate field from newest to oldest', () => { + const transactions = [ + { + bookingDate: '2023-01-01', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-20', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-10', + transactionAmount: mockTransactionAmount, + }, + ]; + expect(sortByBookingDateOrValueDate(transactions)).toEqual([ + { + bookingDate: '2023-01-20', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-10', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-01', + transactionAmount: mockTransactionAmount, + }, + ]); + }); + + it('should sort by valueDate if bookingDate is missing', () => { + const transactions = [ + { + valueDate: '2023-01-01', + transactionAmount: mockTransactionAmount, + }, + { + valueDate: '2023-01-20', + transactionAmount: mockTransactionAmount, + }, + { + valueDate: '2023-01-10', + transactionAmount: mockTransactionAmount, + }, + ]; + expect(sortByBookingDateOrValueDate(transactions)).toEqual([ + { + valueDate: '2023-01-20', + transactionAmount: mockTransactionAmount, + }, + { + valueDate: '2023-01-10', + transactionAmount: mockTransactionAmount, + }, + { + valueDate: '2023-01-01', + transactionAmount: mockTransactionAmount, + }, + ]); + }); + + it('should use bookingDate primarily even if bookingDateTime is on an other date', () => { + const transactions = [ + { + bookingDate: '2023-01-01', + bookingDateTime: '2023-01-01T00:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-10', + bookingDateTime: '2023-01-01T12:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-01', + bookingDateTime: '2023-01-01T12:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-10', + bookingDateTime: '2023-01-01T00:00:00Z', + transactionAmount: mockTransactionAmount, + }, + ]; + expect(sortByBookingDateOrValueDate(transactions)).toEqual([ + { + bookingDate: '2023-01-10', + bookingDateTime: '2023-01-01T12:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-10', + bookingDateTime: '2023-01-01T00:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-01', + bookingDateTime: '2023-01-01T12:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-01', + bookingDateTime: '2023-01-01T00:00:00Z', + transactionAmount: mockTransactionAmount, + }, + ]); + }); + + it('should sort on booking date if value date is widely off', () => { + const transactions = [ + { + bookingDate: '2023-01-01', + valueDateTime: '2023-01-31T00:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-02', + valueDateTime: '2023-01-02T12:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-30', + valueDateTime: '2023-01-01T12:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-30', + valueDateTime: '2023-01-01T00:00:00Z', + transactionAmount: mockTransactionAmount, + }, + ]; + expect(sortByBookingDateOrValueDate(transactions)).toEqual([ + { + bookingDate: '2023-01-30', + valueDateTime: '2023-01-01T12:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-30', + valueDateTime: '2023-01-01T00:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-02', + valueDateTime: '2023-01-02T12:00:00Z', + transactionAmount: mockTransactionAmount, + }, + { + bookingDate: '2023-01-01', + valueDateTime: '2023-01-31T00:00:00Z', + transactionAmount: mockTransactionAmount, + }, + ]); + }); + }); +}); diff --git a/packages/sync-server/src/app-gocardless/util/handle-error.js b/packages/sync-server/src/app-gocardless/util/handle-error.js new file mode 100644 index 00000000000..7aa4519273e --- /dev/null +++ b/packages/sync-server/src/app-gocardless/util/handle-error.js @@ -0,0 +1,16 @@ +import { inspect } from 'util'; + +export function handleError(func) { + return (req, res) => { + func(req, res).catch((err) => { + console.log('Error', req.originalUrl, inspect(err, { depth: null })); + res.send({ + status: 'ok', + data: { + error_code: 'INTERNAL_ERROR', + error_type: err.message ? err.message : 'internal-error', + }, + }); + }); + }; +} diff --git a/packages/sync-server/src/app-gocardless/utils.js b/packages/sync-server/src/app-gocardless/utils.js new file mode 100644 index 00000000000..147276427cd --- /dev/null +++ b/packages/sync-server/src/app-gocardless/utils.js @@ -0,0 +1,45 @@ +export const printIban = (account) => { + if (account.iban) { + return '(XXX ' + account.iban.slice(-4) + ')'; + } else { + return ''; + } +}; + +const compareDates = ( + /** @type {string | number | Date | undefined} */ a, + /** @type {string | number | Date | undefined} */ b, +) => { + if (a == null && b == null) { + return 0; + } else if (a == null) { + return 1; + } else if (b == null) { + return -1; + } + + return +new Date(a) - +new Date(b); +}; + +/** + * @type {(function(*, *): number)[]} + */ +const compareFunctions = [ + (a, b) => compareDates(a.bookingDate, b.bookingDate), + (a, b) => compareDates(a.bookingDateTime, b.bookingDateTime), + (a, b) => compareDates(a.valueDate, b.valueDate), + (a, b) => compareDates(a.valueDateTime, b.valueDateTime), +]; + +export const sortByBookingDateOrValueDate = (transactions = []) => + transactions.sort((a, b) => { + for (const sortFunction of compareFunctions) { + const result = sortFunction(b, a); + if (result !== 0) { + return result; + } + } + return 0; + }); + +export const amountToInteger = (n) => Math.round(n * 100); diff --git a/packages/sync-server/src/app-openid.js b/packages/sync-server/src/app-openid.js new file mode 100644 index 00000000000..487c268b86b --- /dev/null +++ b/packages/sync-server/src/app-openid.js @@ -0,0 +1,101 @@ +import express from 'express'; +import { + errorMiddleware, + requestLoggerMiddleware, + validateSessionMiddleware, +} from './util/middlewares.js'; +import { disableOpenID, enableOpenID, isAdmin } from './account-db.js'; +import { + isValidRedirectUrl, + loginWithOpenIdFinalize, +} from './accounts/openid.js'; +import * as UserService from './services/user-service.js'; + +let app = express(); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(requestLoggerMiddleware); +export { app as handlers }; + +app.post('/enable', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(res.locals.user_id)) { + res.status(403).send({ + status: 'error', + reason: 'forbidden', + details: 'permission-not-found', + }); + return; + } + + let { error } = (await enableOpenID(req.body)) || {}; + + if (error) { + res.status(500).send({ status: 'error', reason: error }); + return; + } + res.send({ status: 'ok' }); +}); + +app.post('/disable', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(res.locals.user_id)) { + res.status(403).send({ + status: 'error', + reason: 'forbidden', + details: 'permission-not-found', + }); + return; + } + + let { error } = (await disableOpenID(req.body)) || {}; + + if (error) { + res.status(401).send({ status: 'error', reason: error }); + return; + } + res.send({ status: 'ok' }); +}); + +app.get('/config', async (req, res) => { + const { cnt: ownerCount } = UserService.getOwnerCount() || {}; + + if (ownerCount > 0) { + res.status(400).send({ status: 'error', reason: 'already-bootstraped' }); + return; + } + + const auth = UserService.getOpenIDConfig(); + + if (!auth) { + res + .status(500) + .send({ status: 'error', reason: 'OpenID configuration not found' }); + return; + } + + try { + const openIdConfig = JSON.parse(auth.extra_data); + res.send({ openId: openIdConfig }); + } catch (error) { + res + .status(500) + .send({ status: 'error', reason: 'Invalid OpenID configuration' }); + } +}); + +app.get('/callback', async (req, res) => { + let { error, url } = await loginWithOpenIdFinalize(req.query); + + if (error) { + res.status(400).send({ status: 'error', reason: error }); + return; + } + + if (!isValidRedirectUrl(url)) { + res.status(400).send({ status: 'error', reason: 'Invalid redirect URL' }); + return; + } + + res.redirect(url); +}); + +app.use(errorMiddleware); diff --git a/packages/sync-server/src/app-secrets.js b/packages/sync-server/src/app-secrets.js new file mode 100644 index 00000000000..3c06bf8a083 --- /dev/null +++ b/packages/sync-server/src/app-secrets.js @@ -0,0 +1,60 @@ +import express from 'express'; +import { secretsService } from './services/secrets-service.js'; +import getAccountDb, { isAdmin } from './account-db.js'; +import { + requestLoggerMiddleware, + validateSessionMiddleware, +} from './util/middlewares.js'; + +const app = express(); + +export { app as handlers }; +app.use(express.json()); +app.use(requestLoggerMiddleware); +app.use(validateSessionMiddleware); + +app.post('/', async (req, res) => { + let method; + try { + const result = getAccountDb().first( + 'SELECT method FROM auth WHERE active = 1', + ); + method = result?.method; + } catch (error) { + console.error('Failed to fetch auth method:', error); + return res.status(500).send({ + status: 'error', + reason: 'database-error', + details: 'Failed to validate authentication method', + }); + } + const { name, value } = req.body; + + if (method === 'openid') { + let canSaveSecrets = isAdmin(res.locals.user_id); + + if (!canSaveSecrets) { + res.status(403).send({ + status: 'error', + reason: 'not-admin', + details: 'You have to be admin to set secrets', + }); + + return; + } + } + + secretsService.set(name, value); + + res.status(200).send({ status: 'ok' }); +}); + +app.get('/:name', async (req, res) => { + const name = req.params.name; + const keyExists = secretsService.exists(name); + if (keyExists) { + res.sendStatus(204); + } else { + res.status(404).send('key not found'); + } +}); diff --git a/packages/sync-server/src/app-simplefin/app-simplefin.js b/packages/sync-server/src/app-simplefin/app-simplefin.js new file mode 100644 index 00000000000..44bc5d53bda --- /dev/null +++ b/packages/sync-server/src/app-simplefin/app-simplefin.js @@ -0,0 +1,392 @@ +import express from 'express'; +import https from 'https'; +import { SecretName, secretsService } from '../services/secrets-service.js'; +import { handleError } from '../app-gocardless/util/handle-error.js'; +import { requestLoggerMiddleware } from '../util/middlewares.js'; + +const app = express(); +export { app as handlers }; +app.use(express.json()); +app.use(requestLoggerMiddleware); + +app.post( + '/status', + handleError(async (req, res) => { + const token = secretsService.get(SecretName.simplefin_token); + const configured = token != null && token !== 'Forbidden'; + + res.send({ + status: 'ok', + data: { + configured: configured, + }, + }); + }), +); + +app.post( + '/accounts', + handleError(async (req, res) => { + let accessKey = secretsService.get(SecretName.simplefin_accessKey); + + try { + if (accessKey == null || accessKey === 'Forbidden') { + const token = secretsService.get(SecretName.simplefin_token); + if (token == null || token === 'Forbidden') { + throw new Error('No token'); + } else { + accessKey = await getAccessKey(token); + secretsService.set(SecretName.simplefin_accessKey, accessKey); + if (accessKey == null || accessKey === 'Forbidden') { + throw new Error('No access key'); + } + } + } + } catch { + invalidToken(res); + return; + } + + try { + const accounts = await getAccounts(accessKey, null, null, null, true); + + res.send({ + status: 'ok', + data: { + accounts: accounts.accounts, + }, + }); + } catch (e) { + serverDown(e, res); + return; + } + }), +); + +app.post( + '/transactions', + handleError(async (req, res) => { + const { accountId, startDate } = req.body; + + const accessKey = secretsService.get(SecretName.simplefin_accessKey); + + if (accessKey == null || accessKey === 'Forbidden') { + invalidToken(res); + return; + } + + if (Array.isArray(accountId) != Array.isArray(startDate)) { + console.log(accountId, startDate); + throw new Error( + 'accountId and startDate must either both be arrays or both be strings', + ); + } + if (Array.isArray(accountId) && accountId.length !== startDate.length) { + console.log(accountId, startDate); + throw new Error('accountId and startDate arrays must be the same length'); + } + + const earliestStartDate = Array.isArray(startDate) + ? startDate.reduce((a, b) => (a < b ? a : b)) + : startDate; + let results; + try { + results = await getTransactions( + accessKey, + Array.isArray(accountId) ? accountId : [accountId], + new Date(earliestStartDate), + ); + } catch (e) { + if (e.message === 'Forbidden') { + invalidToken(res); + } else { + serverDown(e, res); + } + return; + } + + let response = {}; + if (Array.isArray(accountId)) { + for (let i = 0; i < accountId.length; i++) { + const id = accountId[i]; + response[id] = getAccountResponse(results, id, new Date(startDate[i])); + } + } else { + response = getAccountResponse(results, accountId, new Date(startDate)); + } + + if (results.hasError) { + res.send({ + status: 'ok', + data: !Array.isArray(accountId) + ? results.errors[accountId][0] + : { + ...response, + errors: results.errors, + }, + }); + return; + } + + res.send({ + status: 'ok', + data: response, + }); + }), +); + +function logAccountError(results, accountId, data) { + const errors = results.errors[accountId] || []; + errors.push(data); + results.errors[accountId] = errors; + results.hasError = true; +} + +function getAccountResponse(results, accountId, startDate) { + const account = + !results?.accounts || results.accounts.find((a) => a.id === accountId); + if (!account) { + console.log( + `The account "${accountId}" was not found. Here were the accounts returned:`, + ); + if (results?.accounts) + results.accounts.forEach((a) => console.log(`${a.id} - ${a.org.name}`)); + logAccountError(results, accountId, { + error_type: 'ACCOUNT_MISSING', + error_code: 'ACCOUNT_MISSING', + reason: `The account "${accountId}" was not found. Try unlinking and relinking the account.`, + }); + return; + } + + const needsAttention = results.sferrors.find( + (e) => e === `Connection to ${account.org.name} may need attention`, + ); + if (needsAttention) { + logAccountError(results, accountId, { + error_type: 'ACCOUNT_NEEDS_ATTENTION', + error_code: 'ACCOUNT_NEEDS_ATTENTION', + reason: + 'The account needs your attention at SimpleFIN.', + }); + } + + const startingBalance = parseInt(account.balance.replace('.', '')); + const date = getDate(new Date(account['balance-date'] * 1000)); + + const balances = [ + { + balanceAmount: { + amount: account.balance, + currency: account.currency, + }, + balanceType: 'expected', + referenceDate: date, + }, + { + balanceAmount: { + amount: account.balance, + currency: account.currency, + }, + balanceType: 'interimAvailable', + referenceDate: date, + }, + ]; + + const all = []; + const booked = []; + const pending = []; + + for (const trans of account.transactions) { + const newTrans = {}; + + let dateToUse = 0; + + if (trans.pending ?? trans.posted == 0) { + newTrans.booked = false; + dateToUse = trans.transacted_at; + } else { + newTrans.booked = true; + dateToUse = trans.posted; + } + + const transactionDate = new Date(dateToUse * 1000); + + if (transactionDate < startDate) { + continue; + } + + newTrans.date = getDate(transactionDate); + newTrans.payeeName = trans.payee; + newTrans.remittanceInformationUnstructured = trans.description; + newTrans.transactionAmount = { amount: trans.amount, currency: 'USD' }; + newTrans.transactionId = trans.id; + newTrans.valueDate = newTrans.bookingDate; + + if (newTrans.booked) { + booked.push(newTrans); + } else { + pending.push(newTrans); + } + all.push(newTrans); + } + + return { balances, startingBalance, transactions: { all, booked, pending } }; +} + +function invalidToken(res) { + res.send({ + status: 'ok', + data: { + error_type: 'INVALID_ACCESS_TOKEN', + error_code: 'INVALID_ACCESS_TOKEN', + status: 'rejected', + reason: + 'Invalid SimpleFIN access token. Reset the token and re-link any broken accounts.', + }, + }); +} + +function serverDown(e, res) { + console.log(e); + res.send({ + status: 'ok', + data: { + error_type: 'SERVER_DOWN', + error_code: 'SERVER_DOWN', + status: 'rejected', + reason: 'There was an error communicating with SimpleFIN.', + }, + }); +} + +function parseAccessKey(accessKey) { + let scheme = null; + let rest = null; + let auth = null; + let username = null; + let password = null; + let baseUrl = null; + if (!accessKey || !accessKey.match(/^.*\/\/.*:.*@.*$/)) { + console.log(`Invalid SimpleFIN access key: ${accessKey}`); + throw new Error(`Invalid access key`); + } + [scheme, rest] = accessKey.split('//'); + [auth, rest] = rest.split('@'); + [username, password] = auth.split(':'); + baseUrl = `${scheme}//${rest}`; + return { + baseUrl: baseUrl, + username: username, + password: password, + }; +} + +async function getAccessKey(base64Token) { + const token = Buffer.from(base64Token, 'base64').toString(); + const options = { + method: 'POST', + port: 443, + headers: { 'Content-Length': 0 }, + }; + return new Promise((resolve, reject) => { + const req = https.request(new URL(token), options, (res) => { + res.on('data', (d) => { + resolve(d.toString()); + }); + }); + req.on('error', (e) => { + reject(e); + }); + req.end(); + }); +} + +async function getTransactions(accessKey, accounts, startDate, endDate) { + const now = new Date(); + startDate = startDate || new Date(now.getFullYear(), now.getMonth(), 1); + endDate = endDate || new Date(now.getFullYear(), now.getMonth() + 1, 1); + console.log(`${getDate(startDate)} - ${getDate(endDate)}`); + return await getAccounts(accessKey, accounts, startDate, endDate); +} + +function getDate(date) { + return date.toISOString().split('T')[0]; +} + +function normalizeDate(date) { + return (date.valueOf() - date.getTimezoneOffset() * 60 * 1000) / 1000; +} + +async function getAccounts( + accessKey, + accounts, + startDate, + endDate, + noTransactions = false, +) { + const sfin = parseAccessKey(accessKey); + const options = { + headers: { + Authorization: `Basic ${Buffer.from( + `${sfin.username}:${sfin.password}`, + ).toString('base64')}`, + }, + }; + const params = []; + if (!noTransactions) { + if (startDate) { + params.push(`start-date=${normalizeDate(startDate)}`); + } + if (endDate) { + params.push(`end-date=${normalizeDate(endDate)}`); + } + + params.push(`pending=1`); + } else { + params.push(`balances-only=1`); + } + + if (accounts) { + accounts.forEach((id) => { + params.push(`account=${encodeURIComponent(id)}`); + }); + } + + let queryString = ''; + if (params.length > 0) { + queryString += '?' + params.join('&'); + } + return new Promise((resolve, reject) => { + const req = https.request( + new URL(`${sfin.baseUrl}/accounts${queryString}`), + options, + (res) => { + let data = ''; + res.on('data', (d) => { + data += d; + }); + res.on('end', () => { + if (res.statusCode === 403) { + reject(new Error('Forbidden')); + } else { + try { + const results = JSON.parse(data); + results.sferrors = results.errors; + results.hasError = false; + results.errors = {}; + resolve(results); + } catch (e) { + console.log(`Error parsing JSON response: ${data}`); + reject(e); + } + } + }); + }, + ); + req.on('error', (e) => { + reject(e); + }); + req.end(); + }); +} diff --git a/packages/sync-server/src/app-sync.js b/packages/sync-server/src/app-sync.js new file mode 100644 index 00000000000..92fdf301336 --- /dev/null +++ b/packages/sync-server/src/app-sync.js @@ -0,0 +1,395 @@ +import fs from 'node:fs/promises'; +import { Buffer } from 'node:buffer'; +import express from 'express'; +import * as uuid from 'uuid'; +import { + errorMiddleware, + requestLoggerMiddleware, + validateSessionMiddleware, +} from './util/middlewares.js'; +import { getPathForUserFile, getPathForGroupFile } from './util/paths.js'; + +import * as simpleSync from './sync-simple.js'; + +import { SyncProtoBuf } from '@actual-app/crdt'; +import getAccountDb from './account-db.js'; +import { + File, + FilesService, + FileUpdate, +} from './app-sync/services/files-service.js'; +import { FileNotFound } from './app-sync/errors.js'; +import { + validateSyncedFile, + validateUploadedFile, +} from './app-sync/validation.js'; + +const app = express(); +app.use(validateSessionMiddleware); +app.use(errorMiddleware); +app.use(requestLoggerMiddleware); +app.use(express.raw({ type: 'application/actual-sync' })); +app.use(express.raw({ type: 'application/encrypted-file' })); +app.use(express.json()); + +export { app as handlers }; + +const OK_RESPONSE = { status: 'ok' }; + +function boolToInt(deleted) { + return deleted ? 1 : 0; +} + +const verifyFileExists = (fileId, filesService, res, errorObject) => { + try { + return filesService.get(fileId); + } catch (e) { + if (e instanceof FileNotFound) { + //FIXME: error code should be 404. Need to make sure frontend is ok with it. + //TODO: put this into a middleware that checks if FileNotFound is thrown and returns 404 and same error message + // for every FileNotFound error + res.status(400).send(errorObject); + return; + } + throw e; + } +}; + +app.post('/sync', async (req, res) => { + let requestPb; + try { + requestPb = SyncProtoBuf.SyncRequest.deserializeBinary(req.body); + } catch (e) { + console.log('Error parsing sync request', e); + res.status(500); + res.send({ status: 'error', reason: 'internal-error' }); + return; + } + + let fileId = requestPb.getFileid() || null; + let groupId = requestPb.getGroupid() || null; + let keyId = requestPb.getKeyid() || null; + let since = requestPb.getSince() || null; + let messages = requestPb.getMessagesList(); + + if (!since) { + return res.status(422).send({ + details: 'since-required', + reason: 'unprocessable-entity', + status: 'error', + }); + } + + const filesService = new FilesService(getAccountDb()); + + const currentFile = verifyFileExists( + fileId, + filesService, + res, + 'file-not-found', + ); + + if (!currentFile) { + return; + } + + const errorMessage = validateSyncedFile(groupId, keyId, currentFile); + if (errorMessage) { + res.status(400); + res.send(errorMessage); + return; + } + + let { trie, newMessages } = simpleSync.sync(messages, since, groupId); + + // encode it back... + let responsePb = new SyncProtoBuf.SyncResponse(); + responsePb.setMerkle(JSON.stringify(trie)); + newMessages.forEach((msg) => responsePb.addMessages(msg)); + + res.set('Content-Type', 'application/actual-sync'); + res.set('X-ACTUAL-SYNC-METHOD', 'simple'); + res.send(Buffer.from(responsePb.serializeBinary())); +}); + +app.post('/user-get-key', (req, res) => { + if (!res.locals) return; + + let { fileId } = req.body; + + const filesService = new FilesService(getAccountDb()); + const file = verifyFileExists(fileId, filesService, res, 'file-not-found'); + + if (!file) { + return; + } + + res.send({ + status: 'ok', + data: { + id: file.encryptKeyId, + salt: file.encryptSalt, + test: file.encryptTest, + }, + }); +}); + +app.post('/user-create-key', (req, res) => { + let { fileId, keyId, keySalt, testContent } = req.body; + + const filesService = new FilesService(getAccountDb()); + + if (!verifyFileExists(fileId, filesService, res, 'file not found')) { + return; + } + + filesService.update( + fileId, + new FileUpdate({ + encryptSalt: keySalt, + encryptKeyId: keyId, + encryptTest: testContent, + }), + ); + + res.send(OK_RESPONSE); +}); + +app.post('/reset-user-file', async (req, res) => { + let { fileId } = req.body; + + const filesService = new FilesService(getAccountDb()); + const file = verifyFileExists( + fileId, + filesService, + res, + 'User or file not found', + ); + + if (!file) { + return; + } + + const groupId = file.groupId; + + filesService.update(fileId, new FileUpdate({ groupId: null })); + + if (groupId) { + try { + await fs.unlink(getPathForGroupFile(groupId)); + } catch { + console.log(`Unable to delete sync data for group "${groupId}"`); + } + } + + res.send(OK_RESPONSE); +}); + +app.post('/upload-user-file', async (req, res) => { + if (typeof req.headers['x-actual-name'] !== 'string') { + // FIXME: Not sure how this cannot be a string when the header is + // set. + res.status(400).send('single x-actual-name is required'); + return; + } + + let name = decodeURIComponent(req.headers['x-actual-name']); + let fileId = req.headers['x-actual-file-id']; + + if (!fileId || typeof fileId !== 'string') { + res.status(400).send('fileId is required'); + return; + } + + let groupId = req.headers['x-actual-group-id'] || null; + let encryptMeta = req.headers['x-actual-encrypt-meta'] || null; + let syncFormatVersion = req.headers['x-actual-format'] || null; + + let keyId = + encryptMeta && typeof encryptMeta === 'string' + ? JSON.parse(encryptMeta).keyId + : null; + + const filesService = new FilesService(getAccountDb()); + let currentFile; + + try { + currentFile = filesService.get(fileId); + } catch (e) { + if (e instanceof FileNotFound) { + currentFile = null; + } else { + throw e; + } + } + + const errorMessage = validateUploadedFile(groupId, keyId, currentFile); + if (errorMessage) { + res.status(400).send(errorMessage); + return; + } + + try { + await fs.writeFile(getPathForUserFile(fileId), req.body); + } catch (err) { + console.log('Error writing file', err); + res.status(500).send({ status: 'error' }); + return; + } + + if (!currentFile) { + // it's new + groupId = uuid.v4(); + + filesService.set( + new File({ + id: fileId, + groupId: groupId, + syncVersion: syncFormatVersion, + name: name, + encryptMeta: encryptMeta, + owner: + res.locals.user_id || + (() => { + throw new Error('User ID is required for file creation'); + })(), + }), + ); + + res.send({ status: 'ok', groupId }); + return; + } + + if (!groupId) { + // sync state was reset, create new group + groupId = uuid.v4(); + filesService.update(fileId, new FileUpdate({ groupId: groupId })); + } + + // Regardless, update some properties + filesService.update( + fileId, + new FileUpdate({ + syncVersion: syncFormatVersion, + encryptMeta: encryptMeta, + name: name, + }), + ); + + res.send({ status: 'ok', groupId }); +}); + +app.get('/download-user-file', async (req, res) => { + let fileId = req.headers['x-actual-file-id']; + if (typeof fileId !== 'string') { + // FIXME: Not sure how this cannot be a string when the header is + // set. + res.status(400).send('Single file ID is required'); + return; + } + + const filesService = new FilesService(getAccountDb()); + if (!verifyFileExists(fileId, filesService, res, 'User or file not found')) { + return; + } + + res.setHeader('Content-Disposition', `attachment;filename=${fileId}`); + res.sendFile(getPathForUserFile(fileId)); +}); + +app.post('/update-user-filename', (req, res) => { + let { fileId, name } = req.body; + + const filesService = new FilesService(getAccountDb()); + + if (!verifyFileExists(fileId, filesService, res, 'file not found')) { + return; + } + + filesService.update(fileId, new FileUpdate({ name: name })); + res.send(OK_RESPONSE); +}); + +app.get('/list-user-files', (req, res) => { + const fileService = new FilesService(getAccountDb()); + const rows = fileService.find({ userId: res.locals.user_id }); + res.send({ + status: 'ok', + data: rows.map((row) => ({ + deleted: boolToInt(row.deleted), + fileId: row.id, + groupId: row.groupId, + name: row.name, + encryptKeyId: row.encryptKeyId, + owner: row.owner, + usersWithAccess: fileService + .findUsersWithAccess(row.id) + .map((access) => ({ + ...access, + owner: access.userId === row.owner, + })), + })), + }); +}); + +app.get('/get-user-file-info', (req, res) => { + let fileId = req.headers['x-actual-file-id']; + + // TODO: Return 422 if fileId is not provided. Need to make sure frontend can handle it + // if (!fileId) { + // return res.status(422).send({ + // details: 'fileId-required', + // reason: 'unprocessable-entity', + // status: 'error', + // }); + // } + + const fileService = new FilesService(getAccountDb()); + + const file = verifyFileExists(fileId, fileService, res, { + status: 'error', + reason: 'file-not-found', + }); + + if (!file) { + return; + } + + res.send({ + status: 'ok', + data: { + deleted: boolToInt(file.deleted), // FIXME: convert to boolean, make sure it works in the frontend + fileId: file.id, + groupId: file.groupId, + name: file.name, + encryptMeta: file.encryptMeta ? JSON.parse(file.encryptMeta) : null, + usersWithAccess: fileService + .findUsersWithAccess(file.id) + .map((access) => ({ + ...access, + owner: access.userId === file.owner, + })), + }, + }); +}); + +app.post('/delete-user-file', (req, res) => { + let { fileId } = req.body; + + if (!fileId) { + return res.status(422).send({ + details: 'fileId-required', + reason: 'unprocessable-entity', + status: 'error', + }); + } + + const filesService = new FilesService(getAccountDb()); + if (!verifyFileExists(fileId, filesService, res, 'file-not-found')) { + return; + } + + filesService.update(fileId, new FileUpdate({ deleted: true })); + + res.send(OK_RESPONSE); +}); diff --git a/packages/sync-server/src/app-sync.test.js b/packages/sync-server/src/app-sync.test.js new file mode 100644 index 00000000000..3d3dd10bdee --- /dev/null +++ b/packages/sync-server/src/app-sync.test.js @@ -0,0 +1,875 @@ +import fs from 'node:fs'; +import request from 'supertest'; +import { handlers as app } from './app-sync.js'; +import { getPathForUserFile } from './util/paths.js'; +import getAccountDb from './account-db.js'; +import { SyncProtoBuf } from '@actual-app/crdt'; +import crypto from 'node:crypto'; + +const ADMIN_ROLE = 'ADMIN'; + +const createUser = (userId, userName, role, owner = 0, enabled = 1) => { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, owner, role], + ); +}; + +describe('/user-get-key', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).post('/user-get-key'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('returns encryption key details for a given fileId', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const encrypt_salt = 'test-salt'; + const encrypt_keyid = 'test-key-id'; + const encrypt_test = 'test-encrypt-test'; + + getAccountDb().mutate( + 'INSERT INTO files (id, encrypt_salt, encrypt_keyid, encrypt_test, owner) VALUES (?, ?, ?, ?, ?)', + [fileId, encrypt_salt, encrypt_keyid, encrypt_test, 'genericAdmin'], + ); + + const res = await request(app) + .post('/user-get-key') + .set('x-actual-token', 'valid-token') + .send({ fileId }); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual({ + status: 'ok', + data: { + id: encrypt_keyid, + salt: encrypt_salt, + test: encrypt_test, + }, + }); + }); + + it('returns 400 if the file is not found', async () => { + const res = await request(app) + .post('/user-get-key') + .set('x-actual-token', 'valid-token') + .send({ fileId: 'non-existent-file-id' }); + + expect(res.statusCode).toEqual(400); + expect(res.text).toBe('file-not-found'); + }); +}); + +describe('/user-create-key', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).post('/user-create-key'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('returns 400 if the file is not found', async () => { + const res = await request(app) + .post('/user-create-key') + .set('x-actual-token', 'valid-token') + .send({ fileId: 'non-existent-file-id' }); + + expect(res.statusCode).toEqual(400); + expect(res.text).toBe('file not found'); + }); + + it('creates a new encryption key for the file', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + + const old_encrypt_salt = 'old-salt'; + const old_encrypt_keyid = 'old-key'; + const old_encrypt_test = 'old-encrypt-test'; + const encrypt_salt = 'test-salt'; + const encrypt_keyid = 'test-key-id'; + const encrypt_test = 'test-encrypt-test'; + + getAccountDb().mutate( + 'INSERT INTO files (id, encrypt_salt, encrypt_keyid, encrypt_test) VALUES (?, ?, ?, ?)', + [fileId, old_encrypt_salt, old_encrypt_keyid, old_encrypt_test], + ); + + const res = await request(app) + .post('/user-create-key') + .set('x-actual-token', 'valid-token') + .send({ + fileId, + keyId: encrypt_keyid, + keySalt: encrypt_salt, + testContent: encrypt_test, + }); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual({ status: 'ok' }); + + const rows = getAccountDb().all( + 'SELECT encrypt_salt, encrypt_keyid, encrypt_test FROM files WHERE id = ?', + [fileId], + ); + + expect(rows[0].encrypt_salt).toEqual(encrypt_salt); + expect(rows[0].encrypt_keyid).toEqual(encrypt_keyid); + expect(rows[0].encrypt_test).toEqual(encrypt_test); + }); +}); + +describe('/reset-user-file', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).post('/reset-user-file'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('resets the user file and deletes the group file', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const groupId = 'test-group-id'; + + // Use addMockFile to insert a mock file into the database + getAccountDb().mutate( + 'INSERT INTO files (id, group_id, deleted, owner) VALUES (?, ?, FALSE, ?)', + [fileId, groupId, 'genericAdmin'], + ); + + getAccountDb().mutate( + 'INSERT INTO user_access (file_id, user_id) VALUES (?, ?)', + [fileId, 'genericAdmin'], + ); + + const res = await request(app) + .post('/reset-user-file') + .set('x-actual-token', 'valid-token') + .send({ fileId }); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual({ status: 'ok' }); + + // Verify that the file is marked as deleted + const rows = getAccountDb().all('SELECT group_id FROM files WHERE id = ?', [ + fileId, + ]); + + expect(rows[0].group_id).toBeNull(); + }); + + it('returns 400 if the file is not found', async () => { + const res = await request(app) + .post('/reset-user-file') + .set('x-actual-token', 'valid-token') + .send({ fileId: 'non-existent-file-id' }); + + expect(res.statusCode).toEqual(400); + expect(res.text).toBe('User or file not found'); + }); +}); + +describe('/upload-user-file', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).post('/upload-user-file'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('returns 400 if x-actual-name header is missing', async () => { + const res = await request(app) + .post('/upload-user-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', 'test-file-id') + .send('file content'); + + expect(res.statusCode).toEqual(400); + expect(res.text).toBe('single x-actual-name is required'); + }); + + it('returns 400 if fileId is missing', async () => { + const content = Buffer.from('file content'); + const res = await request(app) + .post('/upload-user-file') + .set('Content-Type', 'application/encrypted-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-name', 'test-file') + .send(content); + + expect(res.statusCode).toEqual(400); + expect(res.text).toBe('fileId is required'); + }); + + it('uploads a new file successfully', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const fileName = 'test-file.txt'; + const fileContent = 'test file content'; + const fileContentBuffer = Buffer.from(fileContent); + const syncVersion = 2; + const encryptMeta = JSON.stringify({ keyId: 'key-id' }); + + // Verify that the file does not exist before upload + const rowsBefore = getAccountDb().all('SELECT * FROM files WHERE id = ?', [ + fileId, + ]); + + expect(rowsBefore.length).toBe(0); + + const res = await request(app) + .post('/upload-user-file') + .set('Content-Type', 'application/encrypted-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-name', fileName) + .set('x-actual-file-id', fileId) + .set('x-actual-format', syncVersion.toString()) + .set('x-actual-encrypt-meta', encryptMeta) + .send(fileContentBuffer); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual({ status: 'ok', groupId: expect.any(String) }); + + const receivedGroupid = res.body.groupId; + // Verify that the file exists in the accountDb + const rowsAfter = getAccountDb().all('SELECT * FROM files WHERE id = ?', [ + fileId, + ]); + expect(rowsAfter.length).toBe(1); + expect(rowsAfter[0].id).toEqual(fileId); + expect(rowsAfter[0].group_id).toEqual(receivedGroupid); + expect(rowsAfter[0].sync_version).toEqual(syncVersion); + expect(rowsAfter[0].name).toEqual(fileName); + expect(rowsAfter[0].encrypt_meta).toEqual(encryptMeta); + + // Verify that the file was written to the file system + const filePath = getPathForUserFile(fileId); + const writtenContent = await fs.promises.readFile(filePath, 'utf8'); + expect(writtenContent).toEqual(fileContent); + + // Clean up the file + await fs.promises.unlink(filePath); + }); + + it('uploads and updates an existing file successfully', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const oldGroupId = null; //sync state was reset + const oldFileName = 'old-test-file.txt'; + const newFileName = 'new-test-file.txt'; + const oldFileContent = 'old file content'; + const newFileContent = 'new file content'; + const oldSyncVersion = 1; + const newSyncVersion = 2; + const oldKeyId = 'old-key-id'; + const oldEncryptMeta = JSON.stringify({ keyId: oldKeyId }); + const newEncryptMeta = JSON.stringify({ + keyId: oldKeyId, + sentinelValue: 1, + }); //keep the same key, but change other things + + // Create the old file version + getAccountDb().mutate( + 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_keyid) VALUES (?, ?, ?, ?, ?, ?)', + [ + fileId, + oldGroupId, + oldSyncVersion, + oldFileName, + oldEncryptMeta, + oldKeyId, + ], + ); + + await fs.writeFile(getPathForUserFile(fileId), oldFileContent, (err) => { + if (err) throw err; + }); + + const res = await request(app) + .post('/upload-user-file') + .set('Content-Type', 'application/encrypted-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', fileId) + .set('x-actual-name', newFileName) + .set('x-actual-format', newSyncVersion.toString()) + .set('x-actual-encrypt-meta', newEncryptMeta) + .send(Buffer.from(newFileContent)); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual({ status: 'ok', groupId: expect.any(String) }); + + const receivedGroupid = res.body.groupId; + + // Verify that the file was updated in the accountDb + const rowsAfter = getAccountDb().all('SELECT * FROM files WHERE id = ?', [ + fileId, + ]); + expect(rowsAfter.length).toBe(1); + expect(rowsAfter[0].id).toEqual(fileId); + expect(rowsAfter[0].group_id).toEqual(receivedGroupid); + expect(rowsAfter[0].sync_version).toEqual(newSyncVersion); + expect(rowsAfter[0].name).toEqual(newFileName); + expect(rowsAfter[0].encrypt_meta).toEqual(newEncryptMeta); + + // Verify that the file was written to the file system + const filePath = getPathForUserFile(fileId); + const writtenContent = await fs.promises.readFile(filePath, 'utf8'); + expect(writtenContent).toEqual(newFileContent); + + // Clean up the file + await fs.promises.unlink(filePath); + }); + + it('returns 400 if the file is part of an old group', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const groupId = 'old-group-id'; + const fileName = 'test-file.txt'; + const keyId = 'key-id'; + const syncVersion = 2; + + // Add a mock file with the old group ID + addMockFile( + fileId, + 'current-group-id', + keyId, + JSON.stringify({ keyId }), + syncVersion, + ); + + const res = await request(app) + .post('/upload-user-file') + .set('Content-Type', 'application/encrypted-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', fileId) + .set('x-actual-group-id', groupId) + .set('x-actual-name', fileName); + + expect(res.statusCode).toEqual(400); + expect(res.text).toEqual('file-has-reset'); + }); + + it('returns 400 if the file has a new encryption key', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const groupId = 'group-id'; + const fileName = 'test-file.txt'; + const oldKeyId = 'old-key-id'; + const newKeyId = 'new-key-id'; + const syncVersion = 2; + + // Add a mock file with the new key + addMockFile( + fileId, + groupId, + newKeyId, + JSON.stringify({ newKeyId }), + syncVersion, + ); + + const res = await request(app) + .post('/upload-user-file') + .set('Content-Type', 'application/encrypted-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', fileId) + .set('x-actual-group-id', groupId) + .set('x-actual-name', fileName) + .set('x-actual-encrypt-meta', JSON.stringify({ keyId: oldKeyId })); + + expect(res.statusCode).toEqual(400); + expect(res.text).toEqual('file-has-new-key'); + }); +}); + +describe('/download-user-file', () => { + describe('default version', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).get('/download-user-file'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('returns 401 if the user is invalid', async () => { + const res = await request(app) + .get('/download-user-file') + .set('x-actual-token', 'invalid-token'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('returns 400 error if the file does not exist in the database', async () => { + const res = await request(app) + .get('/download-user-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', 'non-existing-file-id'); + + expect(res.statusCode).toEqual(400); + expect(res.text).toBe('User or file not found'); + }); + + it('returns 500 error if the file does not exist on the filesystem', async () => { + getAccountDb().mutate( + 'INSERT INTO files (id, deleted) VALUES (?, FALSE)', + ['missing-fs-file'], + ); + + const res = await request(app) + .get('/download-user-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', 'missing-fs-file'); + + expect(res.statusCode).toEqual(404); + }); + + it('returns an attachment file', async () => { + const fileContent = 'content'; + fs.writeFileSync(getPathForUserFile('file-id'), fileContent); + getAccountDb().mutate( + 'INSERT INTO files (id, deleted) VALUES (?, FALSE)', + ['file-id'], + ); + + const res = await request(app) + .get('/download-user-file') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', 'file-id'); + + expect(res.statusCode).toEqual(200); + expect(res.headers).toEqual( + expect.objectContaining({ + 'content-disposition': 'attachment;filename=file-id', + 'content-type': 'application/octet-stream', + }), + ); + + expect(res.body).toBeInstanceOf(Buffer); + expect(res.body.toString('utf8')).toEqual(fileContent); + }); + }); +}); + +describe('/update-user-filename', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).post('/update-user-filename'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('returns 400 if the file is not found', async () => { + const res = await request(app) + .post('/update-user-filename') + .set('x-actual-token', 'valid-token') + .send({ fileId: 'non-existent-file-id', name: 'new-filename' }); + + expect(res.statusCode).toEqual(400); + expect(res.text).toBe('file not found'); + }); + + it('successfully updates the filename', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const oldName = 'old-filename'; + const newName = 'new-filename'; + + // Insert a mock file into the database + getAccountDb().mutate( + 'INSERT INTO files (id, name, deleted) VALUES (?, ?, FALSE)', + [fileId, oldName], + ); + + const res = await request(app) + .post('/update-user-filename') + .set('x-actual-token', 'valid-token') + .send({ fileId, name: newName }); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual({ status: 'ok' }); + + // Verify that the filename was updated + const rows = getAccountDb().all('SELECT name FROM files WHERE id = ?', [ + fileId, + ]); + + expect(rows[0].name).toEqual(newName); + }); +}); + +describe('/list-user-files', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).get('/list-user-files'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('returns a list of user files for an authenticated user', async () => { + createUser('fileListAdminId', 'admin', ADMIN_ROLE, 1); + const fileId1 = crypto.randomBytes(16).toString('hex'); + const fileId2 = crypto.randomBytes(16).toString('hex'); + const fileName1 = 'file1.txt'; + const fileName2 = 'file2.txt'; + + // Insert mock files into the database + getAccountDb().mutate( + 'INSERT INTO files (id, name, deleted, owner) VALUES (?, ?, FALSE, ?)', + [fileId1, fileName1, ''], + ); + getAccountDb().mutate( + 'INSERT INTO files (id, name, deleted, owner) VALUES (?, ?, FALSE, ?)', + [fileId2, fileName2, ''], + ); + + const res = await request(app) + .get('/list-user-files') + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual( + expect.objectContaining({ + status: 'ok', + data: expect.arrayContaining([ + expect.objectContaining({ + deleted: 0, + fileId: fileId1, + groupId: null, + name: fileName1, + encryptKeyId: null, + }), + expect.objectContaining({ + deleted: 0, + fileId: fileId2, + groupId: null, + name: fileName2, + encryptKeyId: null, + }), + ]), + }), + ); + }); +}); + +describe('/get-user-file-info', () => { + it('returns file info for a valid fileId', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const groupId = 'test-group-id'; + const fileInfo = { + id: fileId, + group_id: groupId, + name: 'test-file', + encrypt_meta: JSON.stringify({ key: 'value' }), + deleted: 0, + }; + + getAccountDb().mutate( + 'INSERT INTO files (id, group_id, name, encrypt_meta, deleted) VALUES (?, ?, ?, ?, ?)', + [ + fileInfo.id, + fileInfo.group_id, + fileInfo.name, + fileInfo.encrypt_meta, + fileInfo.deleted, + ], + ); + + const res = await request(app) + .get('/get-user-file-info') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', fileId) + .send(); + + expect(res.statusCode).toEqual(200); + + expect(res.body).toEqual({ + status: 'ok', + data: { + deleted: fileInfo.deleted, + fileId: fileInfo.id, + groupId: fileInfo.group_id, + name: fileInfo.name, + encryptMeta: { key: 'value' }, + usersWithAccess: [], + }, + }); + }); + + it('returns error if the file is not found', async () => { + const fileId = 'non-existent-file-id'; + + const res = await request(app) + .get('/get-user-file-info') + .set('x-actual-token', 'valid-token') + .set('x-actual-file-id', fileId); + + expect(res.statusCode).toEqual(400); + expect(res.body).toEqual({ status: 'error', reason: 'file-not-found' }); + }); + + it('returns error if the user is not authenticated', async () => { + // Simulate an unauthenticated request by not setting the necessary headers + const res = await request(app).get('/get-user-file-info'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + status: 'error', + reason: 'unauthorized', + details: 'token-not-found', + }); + }); +}); + +describe('/delete-user-file', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).post('/delete-user-file'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + // it returns 422 if the fileId is not provided + it('returns 422 if the fileId is not provided', async () => { + const res = await request(app) + .post('/delete-user-file') + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(422); + expect(res.body).toEqual({ + details: 'fileId-required', + reason: 'unprocessable-entity', + status: 'error', + }); + }); + + it('returns 400 if the file does not exist', async () => { + const res = await request(app) + .post('/delete-user-file') + .set('x-actual-token', 'valid-token') + .send({ fileId: 'non-existing-file-id' }); + + expect(res.statusCode).toEqual(400); + expect(res.text).toEqual('file-not-found'); + }); + + it('marks the file as deleted', async () => { + const accountDb = getAccountDb(); + const fileId = crypto.randomBytes(16).toString('hex'); + + // Insert a file into the database + accountDb.mutate( + 'INSERT OR IGNORE INTO files (id, deleted) VALUES (?, FALSE)', + [fileId], + ); + + const res = await request(app) + .post('/delete-user-file') + .set('x-actual-token', 'valid-token') + .send({ fileId }); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual({ status: 'ok' }); + + // Verify that the file is marked as deleted + const rows = accountDb.all('SELECT deleted FROM files WHERE id = ?', [ + fileId, + ]); + expect(rows[0].deleted).toBe(1); + }); +}); + +describe('/sync', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).post('/sync'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('returns 200 and syncs successfully with correct file attributes', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const groupId = 'group-id'; + const keyId = 'key-id'; + const syncVersion = 2; + const encryptMeta = JSON.stringify({ keyId }); + + addMockFile(fileId, groupId, keyId, encryptMeta, syncVersion); + + const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId); + + const res = await sendSyncRequest(syncRequest); + + expect(res.statusCode).toEqual(200); + expect(res.headers['content-type']).toEqual('application/actual-sync'); + expect(res.headers['x-actual-sync-method']).toEqual('simple'); + }); + + it('returns 500 if the request body is invalid', async () => { + const res = await request(app) + .post('/sync') + .set('x-actual-token', 'valid-token') + // Content-Type is set correctly, but the body cannot be deserialized + .set('Content-Type', 'application/actual-sync') + .send('invalid-body'); + + expect(res.statusCode).toEqual(500); + expect(res.body).toEqual({ + status: 'error', + reason: 'internal-error', + }); + }); + + it('returns 422 if since is not provided', async () => { + const syncRequest = createMinimalSyncRequest( + 'file-id', + 'group-id', + 'key-id', + ); + syncRequest.setSince(undefined); + + const res = await sendSyncRequest(syncRequest); + + expect(res.statusCode).toEqual(422); + expect(res.body).toEqual({ + status: 'error', + reason: 'unprocessable-entity', + details: 'since-required', + }); + }); + + it('returns 400 if the file does not exist in the database', async () => { + const syncRequest = createMinimalSyncRequest( + 'non-existant-file-id', + 'group-id', + 'key-id', + ); + + // We do not insert the file into the database, so it does not exist + + const res = await sendSyncRequest(syncRequest); + + expect(res.statusCode).toEqual(400); + expect(res.text).toEqual('file-not-found'); + }); + + it('returns 400 if the file sync version is old', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const groupId = 'group-id'; + const keyId = 'key-id'; + const oldSyncVersion = 1; // Assuming SYNC_FORMAT_VERSION is 2 + + // Add a mock file with an old sync version + addMockFile( + fileId, + groupId, + keyId, + JSON.stringify({ keyId }), + oldSyncVersion, + ); + + const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId); + + const res = await sendSyncRequest(syncRequest); + + expect(res.statusCode).toEqual(400); + expect(res.text).toEqual('file-old-version'); + }); + + it('returns 400 if the file needs to be uploaded (no group_id)', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const groupId = null; // No group ID + const keyId = 'key-id'; + const syncVersion = 2; + + addMockFile(fileId, groupId, keyId, JSON.stringify({ keyId }), syncVersion); + + const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId); + + const res = await sendSyncRequest(syncRequest); + + expect(res.statusCode).toEqual(400); + expect(res.text).toEqual('file-needs-upload'); + }); + + it('returns 400 if the file has a new encryption key', async () => { + const fileId = crypto.randomBytes(16).toString('hex'); + const groupId = 'group-id'; + const keyId = 'old-key-id'; + const newKeyId = 'new-key-id'; + const syncVersion = 2; + + // Add a mock file with the old key + addMockFile(fileId, groupId, keyId, JSON.stringify({ keyId }), syncVersion); + + // Create a sync request with the new key + const syncRequest = createMinimalSyncRequest(fileId, groupId, newKeyId); + const res = await sendSyncRequest(syncRequest); + + expect(res.statusCode).toEqual(400); + expect(res.text).toEqual('file-has-new-key'); + }); +}); + +function addMockFile(fileId, groupId, keyId, encryptMeta, syncVersion) { + getAccountDb().mutate( + 'INSERT INTO files (id, group_id, encrypt_keyid, encrypt_meta, sync_version, owner) VALUES (?, ?, ?,?, ?, ?)', + [fileId, groupId, keyId, encryptMeta, syncVersion, 'genericAdmin'], + ); +} + +function createMinimalSyncRequest(fileId, groupId, keyId) { + const syncRequest = new SyncProtoBuf.SyncRequest(); + syncRequest.setFileid(fileId); + syncRequest.setGroupid(groupId); + syncRequest.setKeyid(keyId); + syncRequest.setSince('2024-01-01T00:00:00.000Z'); + syncRequest.setMessagesList([]); + return syncRequest; +} + +async function sendSyncRequest(syncRequest) { + const serializedRequest = syncRequest.serializeBinary(); + // Convert Uint8Array to Buffer + const bufferRequest = Buffer.from(serializedRequest); + + const res = await request(app) + .post('/sync') + .set('x-actual-token', 'valid-token') + .set('Content-Type', 'application/actual-sync') + .send(bufferRequest); + return res; +} diff --git a/packages/sync-server/src/app-sync/errors.js b/packages/sync-server/src/app-sync/errors.js new file mode 100644 index 00000000000..b57867cdda3 --- /dev/null +++ b/packages/sync-server/src/app-sync/errors.js @@ -0,0 +1,13 @@ +export class FileNotFound extends Error { + constructor(params = {}) { + super("File does not exist or you don't have access to it"); + this.details = params; + } +} + +export class GenericFileError extends Error { + constructor(message, params = {}) { + super(message); + this.details = params; + } +} diff --git a/packages/sync-server/src/app-sync/services/files-service.js b/packages/sync-server/src/app-sync/services/files-service.js new file mode 100644 index 00000000000..a01ba417236 --- /dev/null +++ b/packages/sync-server/src/app-sync/services/files-service.js @@ -0,0 +1,243 @@ +import getAccountDb, { isAdmin } from '../../account-db.js'; +import { FileNotFound, GenericFileError } from '../errors.js'; + +class FileBase { + constructor( + name, + groupId, + encryptSalt, + encryptTest, + encryptKeyId, + encryptMeta, + syncVersion, + deleted, + owner, + ) { + this.name = name; + this.groupId = groupId; + this.encryptSalt = encryptSalt; + this.encryptTest = encryptTest; + this.encryptKeyId = encryptKeyId; + this.encryptMeta = encryptMeta; + this.syncVersion = syncVersion; + this.deleted = typeof deleted === 'boolean' ? deleted : Boolean(deleted); + this.owner = owner; + } +} + +class File extends FileBase { + constructor({ + id, + name = null, + groupId = null, + encryptSalt = null, + encryptTest = null, + encryptKeyId = null, + encryptMeta = null, + syncVersion = null, + deleted = false, + owner = null, + }) { + super( + name, + groupId, + encryptSalt, + encryptTest, + encryptKeyId, + encryptMeta, + syncVersion, + deleted, + owner, + ); + this.id = id; + } +} + +/** + * Represents a file update. Will only update the fields that are defined. + * @class + * @extends FileBase + */ +class FileUpdate extends FileBase { + constructor({ + name = undefined, + groupId = undefined, + encryptSalt = undefined, + encryptTest = undefined, + encryptKeyId = undefined, + encryptMeta = undefined, + syncVersion = undefined, + deleted = undefined, + owner = undefined, + }) { + super( + name, + groupId, + encryptSalt, + encryptTest, + encryptKeyId, + encryptMeta, + syncVersion, + deleted, + owner, + ); + } +} + +const boolToInt = (bool) => { + return bool ? 1 : 0; +}; + +class FilesService { + constructor(accountDb) { + this.accountDb = accountDb; + } + + get(fileId) { + const rawFile = this.getRaw(fileId); + if (!rawFile || (rawFile && rawFile.deleted)) { + throw new FileNotFound(); + } + + return this.validate(rawFile); + } + + set(file) { + const deletedInt = boolToInt(file.deleted); + this.accountDb.mutate( + 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted, owner) VALUES (?, ?, ?, ?, ?, ?, ?, ? ,?, ?)', + [ + file.id, + file.groupId, + file.syncVersion.toString(), + file.name, + file.encryptMeta, + file.encryptSalt, + file.encrypt_test, + file.encrypt_keyid, + deletedInt, + file.owner, + ], + ); + } + + find({ userId, limit = 1000 }) { + const canSeeAll = isAdmin(userId); + + return ( + canSeeAll + ? this.accountDb.all('SELECT * FROM files WHERE deleted = 0 LIMIT ?', [ + limit, + ]) + : this.accountDb.all( + `SELECT files.* + FROM files + WHERE files.owner = ? and deleted = 0 + UNION + SELECT files.* + FROM files + JOIN user_access + ON user_access.file_id = files.id + AND user_access.user_id = ? + WHERE files.deleted = 0 LIMIT ?`, + [userId, userId, limit], + ) + ).map(this.validate); + } + + findUsersWithAccess(fileId) { + const userAccess = + this.accountDb.all( + `SELECT UA.user_id as userId, users.display_name displayName, users.user_name userName + FROM files + JOIN user_access UA ON UA.file_id = files.id + JOIN users on users.id = UA.user_id + WHERE files.id = ? + UNION ALL + SELECT users.id, users.display_name, users.user_name + FROM files + JOIN users on users.id = files.owner + WHERE files.id = ? + `, + [fileId, fileId], + ) || []; + + return userAccess; + } + + update(id, fileUpdate) { + let query = 'UPDATE files SET'; + const params = []; + const updates = []; + + if (fileUpdate.name !== undefined) { + updates.push('name = ?'); + params.push(fileUpdate.name); + } + if (fileUpdate.groupId !== undefined) { + updates.push('group_id = ?'); + params.push(fileUpdate.groupId); + } + if (fileUpdate.encryptSalt !== undefined) { + updates.push('encrypt_salt = ?'); + params.push(fileUpdate.encryptSalt); + } + if (fileUpdate.encryptTest !== undefined) { + updates.push('encrypt_test = ?'); + params.push(fileUpdate.encryptTest); + } + if (fileUpdate.encryptKeyId !== undefined) { + updates.push('encrypt_keyid = ?'); + params.push(fileUpdate.encryptKeyId); + } + if (fileUpdate.encryptMeta !== undefined) { + updates.push('encrypt_meta = ?'); + params.push(fileUpdate.encryptMeta); + } + if (fileUpdate.syncVersion !== undefined) { + updates.push('sync_version = ?'); + params.push(fileUpdate.syncVersion); + } + if (fileUpdate.deleted !== undefined) { + updates.push('deleted = ?'); + params.push(boolToInt(fileUpdate.deleted)); + } + + if (updates.length > 0) { + query += ' ' + updates.join(', ') + ' WHERE id = ?'; + params.push(id); + + const res = this.accountDb.mutate(query, params); + + if (res.changes != 1) { + throw new GenericFileError('Could not update File', { id }); + } + } + + // Return the modified object + return this.validate(this.getRaw(id)); + } + + getRaw(fileId) { + return this.accountDb.first(`SELECT * FROM files WHERE id = ?`, [fileId]); + } + + validate(rawFile) { + return new File({ + id: rawFile.id, + name: rawFile.name, + groupId: rawFile.group_id, + encryptSalt: rawFile.encrypt_salt, + encryptTest: rawFile.encrypt_test, + encryptKeyId: rawFile.encrypt_keyid, + encryptMeta: rawFile.encrypt_meta, + syncVersion: rawFile.sync_version, + deleted: Boolean(rawFile.deleted), + owner: rawFile.owner, + }); + } +} + +const filesService = new FilesService(getAccountDb()); + +export { filesService, FilesService, File, FileUpdate }; diff --git a/packages/sync-server/src/app-sync/tests/services/files-service.test.js b/packages/sync-server/src/app-sync/tests/services/files-service.test.js new file mode 100644 index 00000000000..9807d99a0d4 --- /dev/null +++ b/packages/sync-server/src/app-sync/tests/services/files-service.test.js @@ -0,0 +1,249 @@ +import getAccountDb from '../../../account-db.js'; +import { FileNotFound } from '../../errors.js'; +import { + FilesService, + File, + FileUpdate, +} from '../../services/files-service.js'; // Adjust the path as necessary +import crypto from 'node:crypto'; +describe('FilesService', () => { + let filesService; + let accountDb; + + const insertToyExampleData = () => { + accountDb.mutate( + 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [ + '1', + 'group1', + 1, + 'file1', + '{"key":"value"}', + 'salt', + 'test', + 'keyid', + 0, + ], + ); + }; + + const clearDatabase = () => { + accountDb.mutate('DELETE FROM user_access'); + accountDb.mutate('DELETE FROM files'); + }; + + beforeAll((done) => { + accountDb = getAccountDb(); + filesService = new FilesService(accountDb); + done(); + }); + + beforeEach((done) => { + insertToyExampleData(); + done(); + }); + + afterEach((done) => { + clearDatabase(); + done(); + }); + + test('get should return a file', () => { + const file = filesService.get('1'); + const expectedFile = new File({ + id: '1', + groupId: 'group1', + syncVersion: 1, + name: 'file1', + encryptMeta: '{"key":"value"}', + encryptSalt: 'salt', + encryptTest: 'test', + encryptKeyId: 'keyid', + deleted: false, + }); + + expect(file).toEqual(expectedFile); + }); + + test('get should throw FileNotFound if file is deleted or does not exist', () => { + const fileId = crypto.randomBytes(16).toString('hex'); + accountDb.mutate( + 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [ + fileId, + 'group1', + 1, + 'file1', + '{"key":"value"}', + 'salt', + 'test', + 'keyid', + 1, + ], + ); + + expect(() => { + filesService.get(fileId); + }).toThrow(FileNotFound); + + expect(() => { + filesService.get(crypto.randomBytes(16).toString('hex')); + }).toThrow(FileNotFound); + }); + + test.each([true, false])( + 'set should insert a new file with deleted: %p', + (deleted) => { + const fileId = crypto.randomBytes(16).toString('hex'); + const newFile = new File({ + id: fileId, + groupId: 'group2', + syncVersion: 1, + name: 'file2', + encryptMeta: '{"key":"value2"}', + deleted: deleted, + }); + + filesService.set(newFile); + + const file = filesService.validate(filesService.getRaw(fileId)); + const expectedFile = new File({ + id: fileId, + groupId: 'group2', + syncVersion: 1, + name: 'file2', + encryptMeta: '{"key":"value2"}', + encryptSalt: null, // default value + encryptTest: null, // default value + encryptKeyId: null, // default value + deleted: deleted, + }); + + expect(file).toEqual(expectedFile); + }, + ); + + test('find should return a list of files', () => { + const files = filesService.find({ userId: 'genericAdmin' }); + expect(files.length).toBe(1); + expect(files[0]).toEqual( + new File({ + id: '1', + groupId: 'group1', + syncVersion: 1, + name: 'file1', + encryptMeta: '{"key":"value"}', + encryptSalt: 'salt', + encryptTest: 'test', + encryptKeyId: 'keyid', + deleted: false, + }), + ); + }); + + test('find should respect the limit parameter', () => { + filesService.set( + new File({ + id: crypto.randomBytes(16).toString('hex'), + groupId: 'group2', + syncVersion: 1, + name: 'file2', + encryptMeta: '{"key":"value2"}', + deleted: false, + }), + ); + // Make sure that the file was inserted + const allFiles = filesService.find({ userId: 'genericAdmin' }); + expect(allFiles.length).toBe(2); + + // Limit the number of files returned + const limitedFiles = filesService.find({ + userId: 'genericAdmin', + limit: 1, + }); + expect(limitedFiles.length).toBe(1); + }); + + test('update should modify all attributes of an existing file', () => { + const fileUpdate = new FileUpdate({ + name: 'updatedFile1', + groupId: 'updatedGroup1', + encryptSalt: 'updatedSalt', + encryptTest: 'updatedTest', + encryptKeyId: 'updatedKeyId', + encryptMeta: '{"key":"updatedValue"}', + syncVersion: 2, + deleted: true, + }); + const updatedFile = filesService.update('1', fileUpdate); + + expect(updatedFile).toEqual( + new File({ + id: '1', + name: 'updatedFile1', + groupId: 'updatedGroup1', + encryptSalt: 'updatedSalt', + encryptTest: 'updatedTest', + encryptMeta: '{"key":"updatedValue"}', + encryptKeyId: 'updatedKeyId', + syncVersion: 2, + deleted: true, + }), + ); + }); + + test('find should return only files accessible to the user', () => { + filesService.set( + new File({ + id: crypto.randomBytes(16).toString('hex'), + groupId: 'group2', + syncVersion: 1, + name: 'file2', + encryptMeta: '{"key":"value2"}', + deleted: false, + owner: 'genericAdmin', + }), + ); + + filesService.set( + new File({ + id: crypto.randomBytes(16).toString('hex'), + groupId: 'group2', + syncVersion: 1, + name: 'file2', + encryptMeta: '{"key":"value2"}', + deleted: false, + owner: 'genericUser', + }), + ); + + expect(filesService.find({ userId: 'genericUser' })).toHaveLength(1); + expect( + filesService.find({ userId: 'genericAdmin' }).length, + ).toBeGreaterThan(1); + }); + + test.each([['update-group', null]])( + 'update should modify a single attribute with groupId = $groupId', + (newGroupId) => { + const fileUpdate = new FileUpdate({ + groupId: newGroupId, + }); + const updatedFile = filesService.update('1', fileUpdate); + + expect(updatedFile).toEqual( + new File({ + id: '1', + name: 'file1', + groupId: newGroupId, + syncVersion: 1, + encryptMeta: '{"key":"value"}', + encryptSalt: 'salt', + encryptTest: 'test', + encryptKeyId: 'keyid', + deleted: false, + }), + ); + }, + ); +}); diff --git a/packages/sync-server/src/app-sync/validation.js b/packages/sync-server/src/app-sync/validation.js new file mode 100644 index 00000000000..4fe3fff2c02 --- /dev/null +++ b/packages/sync-server/src/app-sync/validation.js @@ -0,0 +1,77 @@ +// This is a version representing the internal format of sync +// messages. When this changes, all sync files need to be reset. We +// will check this version when syncing and notify the user if they +// need to reset. +const SYNC_FORMAT_VERSION = 2; + +const validateSyncedFile = (groupId, keyId, currentFile) => { + if ( + currentFile.syncVersion == null || + currentFile.syncVersion < SYNC_FORMAT_VERSION + ) { + return 'file-old-version'; + } + + // When resetting sync state, something went wrong. There is no + // group id and it's awaiting a file to be uploaded. + if (currentFile.groupId == null) { + return 'file-needs-upload'; + } + + // Check to make sure the uploaded file is valid and has been + // encrypted with the same key it is registered with (this might + // be wrong if there was an error during the key creation + // process) + let uploadedKeyId = currentFile.encryptMeta + ? JSON.parse(currentFile.encryptMeta).keyId + : null; + if (uploadedKeyId !== currentFile.encryptKeyId) { + return 'file-key-mismatch'; + } + + // The changes being synced are part of an old group, which + // means the file has been reset. User needs to re-download. + if (groupId !== currentFile.groupId) { + return 'file-has-reset'; + } + + // The data is encrypted with a different key which is + // unacceptable. We can't accept these changes. Reject them and + // tell the user that they need to generate the correct key + // (which necessitates a sync reset so they need to re-download). + if (keyId !== currentFile.encryptKeyId) { + return 'file-has-new-key'; + } + + return null; +}; + +const validateUploadedFile = (groupId, keyId, currentFile) => { + if (!currentFile) { + // File is new, so no need to validate + return null; + } + // The uploading file is part of an old group, so reject + // it. All of its internal sync state is invalid because its + // old. The sync state has been reset, so user needs to + // either reset again or download from the current group. + if (groupId !== currentFile.groupId) { + return 'file-has-reset'; + } + + // The key that the file is encrypted with is different than + // the current registered key. All data must always be + // encrypted with the registered key for consistency. Key + // changes always necessitate a sync reset, which means this + // upload is trying to overwrite another reset. That might + // be be fine, but since we definitely cannot accept a file + // encrypted with the wrong key, we bail and suggest the + // user download the latest file. + if (keyId !== currentFile.encryptKeyId) { + return 'file-has-new-key'; + } + + return null; +}; + +export { validateSyncedFile, validateUploadedFile }; diff --git a/packages/sync-server/src/app.js b/packages/sync-server/src/app.js new file mode 100644 index 00000000000..80504f14d41 --- /dev/null +++ b/packages/sync-server/src/app.js @@ -0,0 +1,93 @@ +import fs from 'node:fs'; +import express from 'express'; +import actuator from 'express-actuator'; +import bodyParser from 'body-parser'; +import cors from 'cors'; +import config from './load-config.js'; +import rateLimit from 'express-rate-limit'; + +import * as accountApp from './app-account.js'; +import * as syncApp from './app-sync.js'; +import * as goCardlessApp from './app-gocardless/app-gocardless.js'; +import * as simpleFinApp from './app-simplefin/app-simplefin.js'; +import * as secretApp from './app-secrets.js'; +import * as adminApp from './app-admin.js'; +import * as openidApp from './app-openid.js'; + +const app = express(); + +process.on('unhandledRejection', (reason) => { + console.log('Rejection:', reason); +}); + +app.disable('x-powered-by'); +app.use(cors()); +app.use( + rateLimit({ + windowMs: 60 * 1000, + max: 500, + legacyHeaders: false, + standardHeaders: true, + }), +); +app.use(bodyParser.json({ limit: `${config.upload.fileSizeLimitMB}mb` })); +app.use( + bodyParser.raw({ + type: 'application/actual-sync', + limit: `${config.upload.fileSizeSyncLimitMB}mb`, + }), +); +app.use( + bodyParser.raw({ + type: 'application/encrypted-file', + limit: `${config.upload.syncEncryptedFileSizeLimitMB}mb`, + }), +); + +app.use('/sync', syncApp.handlers); +app.use('/account', accountApp.handlers); +app.use('/gocardless', goCardlessApp.handlers); +app.use('/simplefin', simpleFinApp.handlers); +app.use('/secret', secretApp.handlers); + +app.use('/admin', adminApp.handlers); +app.use('/openid', openidApp.handlers); + +app.get('/mode', (req, res) => { + res.send(config.mode); +}); + +app.use(actuator()); // Provides /health, /metrics, /info + +// The web frontend +app.use((req, res, next) => { + res.set('Cross-Origin-Opener-Policy', 'same-origin'); + res.set('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); +app.use(express.static(config.webRoot, { index: false })); + +app.get('/*', (req, res) => res.sendFile(config.webRoot + '/index.html')); + +function parseHTTPSConfig(value) { + if (value.startsWith('-----BEGIN')) { + return value; + } + return fs.readFileSync(value); +} + +export default async function run() { + if (config.https) { + const https = await import('node:https'); + const httpsOptions = { + ...config.https, + key: parseHTTPSConfig(config.https.key), + cert: parseHTTPSConfig(config.https.cert), + }; + https.createServer(httpsOptions, app).listen(config.port, config.hostname); + } else { + app.listen(config.port, config.hostname); + } + + console.log('Listening on ' + config.hostname + ':' + config.port + '...'); +} diff --git a/packages/sync-server/src/config-types.ts b/packages/sync-server/src/config-types.ts new file mode 100644 index 00000000000..3feecc9ec8d --- /dev/null +++ b/packages/sync-server/src/config-types.ts @@ -0,0 +1,39 @@ +import { ServerOptions } from 'https'; + +export interface Config { + mode: 'test' | 'development'; + loginMethod: 'password' | 'header' | 'openid'; + trustedProxies: string[]; + dataDir: string; + projectRoot: string; + port: number; + hostname: string; + serverFiles: string; + userFiles: string; + webRoot: string; + https?: { + key: string; + cert: string; + } & ServerOptions; + upload?: { + fileSizeSyncLimitMB: number; + syncEncryptedFileSizeLimitMB: number; + fileSizeLimitMB: number; + }; + openId?: { + issuer: + | string + | { + name: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + }; + client_id: string; + client_secret: string; + server_hostname: string; + authMethod?: 'openid' | 'oauth2'; + }; + multiuser: boolean; + token_expiration?: 'never' | 'openid-provider' | number; +} diff --git a/packages/sync-server/src/db.js b/packages/sync-server/src/db.js new file mode 100644 index 00000000000..a4d57a6a9c8 --- /dev/null +++ b/packages/sync-server/src/db.js @@ -0,0 +1,58 @@ +import Database from 'better-sqlite3'; + +class WrappedDatabase { + constructor(db) { + this.db = db; + } + + /** + * @param {string} sql + * @param {string[]} params + */ + all(sql, params = []) { + let stmt = this.db.prepare(sql); + return stmt.all(...params); + } + + /** + * @param {string} sql + * @param {string[]} params + */ + first(sql, params = []) { + let rows = this.all(sql, params); + return rows.length === 0 ? null : rows[0]; + } + + /** + * @param {string} sql + */ + exec(sql) { + return this.db.exec(sql); + } + + /** + * @param {string} sql + * @param {string[]} params + */ + mutate(sql, params = []) { + let stmt = this.db.prepare(sql); + let info = stmt.run(...params); + return { changes: info.changes, insertId: info.lastInsertRowid }; + } + + /** + * @param {() => void} fn + */ + transaction(fn) { + return this.db.transaction(fn)(); + } + + close() { + this.db.close(); + } +} + +/** @param {string} filename */ +export default function openDatabase(filename) { + return new WrappedDatabase(new Database(filename)); +} diff --git a/packages/sync-server/src/load-config.js b/packages/sync-server/src/load-config.js new file mode 100644 index 00000000000..9c8ee34f90c --- /dev/null +++ b/packages/sync-server/src/load-config.js @@ -0,0 +1,227 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import createDebug from 'debug'; + +const debug = createDebug('actual:config'); +const debugSensitive = createDebug('actual-sensitive:config'); + +const projectRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +debug(`project root: '${projectRoot}'`); +export const sqlDir = path.join(projectRoot, 'src', 'sql'); + +let defaultDataDir = fs.existsSync('/data') ? '/data' : projectRoot; + +if (process.env.ACTUAL_DATA_DIR) { + defaultDataDir = process.env.ACTUAL_DATA_DIR; +} + +debug(`default data directory: '${defaultDataDir}'`); + +function parseJSON(path, allowMissing = false) { + let text; + try { + text = fs.readFileSync(path, 'utf8'); + } catch (e) { + if (allowMissing) { + debug(`config file '${path}' not found, ignoring.`); + return {}; + } + throw e; + } + return JSON.parse(text); +} + +let userConfig; +if (process.env.ACTUAL_CONFIG_PATH) { + debug( + `loading config from ACTUAL_CONFIG_PATH: '${process.env.ACTUAL_CONFIG_PATH}'`, + ); + userConfig = parseJSON(process.env.ACTUAL_CONFIG_PATH); + + defaultDataDir = userConfig.dataDir ?? defaultDataDir; +} else { + let configFile = path.join(projectRoot, 'config.json'); + + if (!fs.existsSync(configFile)) { + configFile = path.join(defaultDataDir, 'config.json'); + } + + debug(`loading config from default path: '${configFile}'`); + userConfig = parseJSON(configFile, true); +} + +/** @type {Omit} */ +let defaultConfig = { + loginMethod: 'password', + // assume local networks are trusted for header authentication + trustedProxies: [ + '10.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + 'fc00::/7', + '::1/128', + ], + port: 5006, + hostname: '::', + webRoot: path.join( + projectRoot, + 'node_modules', + '@actual-app', + 'web', + 'build', + ), + upload: { + fileSizeSyncLimitMB: 20, + syncEncryptedFileSizeLimitMB: 50, + fileSizeLimitMB: 20, + }, + projectRoot, + multiuser: false, + token_expiration: 'never', +}; + +/** @type {import('./config-types.js').Config} */ +let config; +if (process.env.NODE_ENV === 'test') { + config = { + mode: 'test', + dataDir: projectRoot, + serverFiles: path.join(projectRoot, 'test-server-files'), + userFiles: path.join(projectRoot, 'test-user-files'), + ...defaultConfig, + }; +} else { + config = { + mode: 'development', + ...defaultConfig, + dataDir: defaultDataDir, + serverFiles: path.join(defaultDataDir, 'server-files'), + userFiles: path.join(defaultDataDir, 'user-files'), + ...(userConfig || {}), + }; +} + +const finalConfig = { + ...config, + loginMethod: process.env.ACTUAL_LOGIN_METHOD + ? process.env.ACTUAL_LOGIN_METHOD.toLowerCase() + : config.loginMethod, + multiuser: process.env.ACTUAL_MULTIUSER + ? (() => { + const value = process.env.ACTUAL_MULTIUSER.toLowerCase(); + if (!['true', 'false'].includes(value)) { + throw new Error('ACTUAL_MULTIUSER must be either "true" or "false"'); + } + return value === 'true'; + })() + : config.multiuser, + trustedProxies: process.env.ACTUAL_TRUSTED_PROXIES + ? process.env.ACTUAL_TRUSTED_PROXIES.split(',').map((q) => q.trim()) + : config.trustedProxies, + port: +process.env.ACTUAL_PORT || +process.env.PORT || config.port, + hostname: process.env.ACTUAL_HOSTNAME || config.hostname, + serverFiles: process.env.ACTUAL_SERVER_FILES || config.serverFiles, + userFiles: process.env.ACTUAL_USER_FILES || config.userFiles, + webRoot: process.env.ACTUAL_WEB_ROOT || config.webRoot, + https: + process.env.ACTUAL_HTTPS_KEY && process.env.ACTUAL_HTTPS_CERT + ? { + key: process.env.ACTUAL_HTTPS_KEY.replace(/\\n/g, '\n'), + cert: process.env.ACTUAL_HTTPS_CERT.replace(/\\n/g, '\n'), + ...(config.https || {}), + } + : config.https, + upload: + process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB || + process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || + process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB + ? { + fileSizeSyncLimitMB: + +process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB || + +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || + config.upload.fileSizeSyncLimitMB, + syncEncryptedFileSizeLimitMB: + +process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || + +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || + config.upload.syncEncryptedFileSizeLimitMB, + fileSizeLimitMB: + +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || + config.upload.fileSizeLimitMB, + } + : config.upload, + openId: (() => { + if ( + !process.env.ACTUAL_OPENID_DISCOVERY_URL && + !process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT + ) { + return config.openId; + } + const baseConfig = process.env.ACTUAL_OPENID_DISCOVERY_URL + ? { issuer: process.env.ACTUAL_OPENID_DISCOVERY_URL } + : { + ...(() => { + const required = { + authorization_endpoint: + process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT, + token_endpoint: process.env.ACTUAL_OPENID_TOKEN_ENDPOINT, + userinfo_endpoint: process.env.ACTUAL_OPENID_USERINFO_ENDPOINT, + }; + const missing = Object.entries(required) + .filter(([_, value]) => !value) + .map(([key]) => key); + if (missing.length > 0) { + throw new Error( + `Missing required OpenID configuration: ${missing.join(', ')}`, + ); + } + return {}; + })(), + issuer: { + name: process.env.ACTUAL_OPENID_PROVIDER_NAME, + authorization_endpoint: + process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT, + token_endpoint: process.env.ACTUAL_OPENID_TOKEN_ENDPOINT, + userinfo_endpoint: process.env.ACTUAL_OPENID_USERINFO_ENDPOINT, + }, + }; + return { + ...baseConfig, + client_id: + process.env.ACTUAL_OPENID_CLIENT_ID ?? config.openId?.client_id, + client_secret: + process.env.ACTUAL_OPENID_CLIENT_SECRET ?? config.openId?.client_secret, + server_hostname: + process.env.ACTUAL_OPENID_SERVER_HOSTNAME ?? + config.openId?.server_hostname, + }; + })(), + token_expiration: process.env.ACTUAL_TOKEN_EXPIRATION + ? process.env.ACTUAL_TOKEN_EXPIRATION + : config.token_expiration, +}; +debug(`using port ${finalConfig.port}`); +debug(`using hostname ${finalConfig.hostname}`); +debug(`using data directory ${finalConfig.dataDir}`); +debug(`using server files directory ${finalConfig.serverFiles}`); +debug(`using user files directory ${finalConfig.userFiles}`); +debug(`using web root directory ${finalConfig.webRoot}`); +debug(`using login method ${finalConfig.loginMethod}`); +debug(`using trusted proxies ${finalConfig.trustedProxies.join(', ')}`); + +if (finalConfig.https) { + debug(`using https key: ${'*'.repeat(finalConfig.https.key.length)}`); + debugSensitive(`using https key ${finalConfig.https.key}`); + debug(`using https cert: ${'*'.repeat(finalConfig.https.cert.length)}`); + debugSensitive(`using https cert ${finalConfig.https.cert}`); +} + +if (finalConfig.upload) { + debug(`using file sync limit ${finalConfig.upload.fileSizeSyncLimitMB}mb`); + debug( + `using sync encrypted file limit ${finalConfig.upload.syncEncryptedFileSizeLimitMB}mb`, + ); + debug(`using file limit ${finalConfig.upload.fileSizeLimitMB}mb`); +} + +export default finalConfig; diff --git a/packages/sync-server/src/migrations.js b/packages/sync-server/src/migrations.js new file mode 100644 index 00000000000..cba7db0fd75 --- /dev/null +++ b/packages/sync-server/src/migrations.js @@ -0,0 +1,34 @@ +import migrate from 'migrate'; +import path from 'node:path'; +import config from './load-config.js'; + +export default function run(direction = 'up') { + console.log( + `Checking if there are any migrations to run for direction "${direction}"...`, + ); + + return new Promise((resolve) => + migrate.load( + { + stateStore: `${path.join(config.dataDir, '.migrate')}${ + config.mode === 'test' ? '-test' : '' + }`, + migrationsDirectory: `${path.join(config.projectRoot, 'migrations')}`, + }, + (err, set) => { + if (err) { + throw err; + } + + set[direction]((err) => { + if (err) { + throw err; + } + + console.log('Migrations: DONE'); + resolve(); + }); + }, + ), + ); +} diff --git a/packages/sync-server/src/run-migrations.js b/packages/sync-server/src/run-migrations.js new file mode 100644 index 00000000000..b5ed269401a --- /dev/null +++ b/packages/sync-server/src/run-migrations.js @@ -0,0 +1,8 @@ +import run from './migrations.js'; + +const direction = process.argv[2] || 'up'; + +run(direction).catch((err) => { + console.error('Migration failed:', err); + process.exit(1); +}); diff --git a/packages/sync-server/src/scripts/disable-openid.js b/packages/sync-server/src/scripts/disable-openid.js new file mode 100644 index 00000000000..b736b3999f7 --- /dev/null +++ b/packages/sync-server/src/scripts/disable-openid.js @@ -0,0 +1,44 @@ +import { + disableOpenID, + getActiveLoginMethod, + needsBootstrap, +} from '../account-db.js'; +import { promptPassword } from '../util/prompt.js'; + +if (needsBootstrap()) { + console.log('System needs to be bootstrapped first. OpenID is not enabled.'); + + process.exit(1); +} else { + console.log('To disable OpenID, you have to enter your server password:'); + try { + const loginMethod = getActiveLoginMethod(); + console.log(`Current login method: ${loginMethod}`); + + if (loginMethod === 'password') { + console.log('OpenID already disabled.'); + process.exit(0); + } + + const password = await promptPassword(); + const { error } = (await disableOpenID({ password })) || {}; + + if (error) { + console.log('Error disabling OpenID:', error); + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + process.exit(2); + } + console.log('OpenID disabled!'); + console.log( + 'Note: you will need to log in with the password on any browsers or devices that are currently logged in.', + ); + } catch (err) { + console.log('Unexpected error:', err); + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + process.exit(2); + } +} diff --git a/packages/sync-server/src/scripts/enable-openid.js b/packages/sync-server/src/scripts/enable-openid.js new file mode 100644 index 00000000000..caf782ff1a0 --- /dev/null +++ b/packages/sync-server/src/scripts/enable-openid.js @@ -0,0 +1,53 @@ +import { + enableOpenID, + getActiveLoginMethod, + needsBootstrap, +} from '../account-db.js'; +import finalConfig from '../load-config.js'; + +if (needsBootstrap()) { + console.log( + 'It looks like you don’t have a password set yet. Password is the fallback authentication method when using OpenID. Execute the command reset-password before using this command!', + ); + + process.exit(1); +} else { + console.log('Enabling openid based on Environment variables or config.json'); + try { + const loginMethod = getActiveLoginMethod(); + console.log(`Current login method: ${loginMethod}`); + + if (loginMethod === 'openid') { + console.log('OpenID already enabled.'); + process.exit(0); + } + const { error } = (await enableOpenID(finalConfig)) || {}; + + if (error) { + console.log('Error enabling openid:', error); + if (error === 'invalid-login-settings') { + console.log( + 'Error configuring OpenID. Please verify that the configuration file or environment variables are correct.', + ); + + process.exit(1); + } else { + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + + process.exit(2); + } + } + console.log('OpenID enabled!'); + console.log( + 'Note: The first user to login with OpenID will be the owner of the server.', + ); + } catch (err) { + console.log('Unexpected error:', err); + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + process.exit(2); + } +} diff --git a/packages/sync-server/src/scripts/health-check.js b/packages/sync-server/src/scripts/health-check.js new file mode 100644 index 00000000000..6cce6d1352d --- /dev/null +++ b/packages/sync-server/src/scripts/health-check.js @@ -0,0 +1,20 @@ +import fetch from 'node-fetch'; +import config from '../load-config.js'; + +let protocol = config.https ? 'https' : 'http'; +let hostname = config.hostname === '::' ? 'localhost' : config.hostname; + +fetch(`${protocol}://${hostname}:${config.port}/health`) + .then((res) => res.json()) + .then((res) => { + if (res.status !== 'UP') { + throw new Error( + 'Health check failed: Server responded to health check with status ' + + res.status, + ); + } + }) + .catch((err) => { + console.log('Health check failed:', err); + process.exit(1); + }); diff --git a/packages/sync-server/src/scripts/reset-password.js b/packages/sync-server/src/scripts/reset-password.js new file mode 100644 index 00000000000..142269a9f7f --- /dev/null +++ b/packages/sync-server/src/scripts/reset-password.js @@ -0,0 +1,51 @@ +import { bootstrap, needsBootstrap } from '../account-db.js'; +import { changePassword } from '../accounts/password.js'; +import { promptPassword } from '../util/prompt.js'; + +if (needsBootstrap()) { + console.log( + 'It looks like you don’t have a password set yet. Let’s set one up now!', + ); + + try { + const password = await promptPassword(); + const { error } = await bootstrap({ password }); + if (error) { + console.log('Error setting password:', error); + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + process.exit(1); + } + console.log('Password set!'); + } catch (err) { + console.log('Unexpected error:', err); + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + process.exit(1); + } +} else { + console.log('It looks like you already have a password set. Let’s reset it!'); + try { + const password = await promptPassword(); + const { error } = await changePassword(password); + if (error) { + console.log('Error changing password:', error); + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + process.exit(1); + } + console.log('Password changed!'); + console.log( + 'Note: you will need to log in with the new password on any browsers or devices that are currently logged in.', + ); + } catch (err) { + console.log('Unexpected error:', err); + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + process.exit(1); + } +} diff --git a/packages/sync-server/src/secrets.test.js b/packages/sync-server/src/secrets.test.js new file mode 100644 index 00000000000..34a34ed083d --- /dev/null +++ b/packages/sync-server/src/secrets.test.js @@ -0,0 +1,82 @@ +import { secretsService } from './services/secrets-service.js'; +import request from 'supertest'; +import { handlers as app } from './app-secrets.js'; +describe('secretsService', () => { + const testSecretName = 'testSecret'; + const testSecretValue = 'testValue'; + + it('should set a secret', () => { + const result = secretsService.set(testSecretName, testSecretValue); + expect(result).toBeDefined(); + expect(result.changes).toBe(1); + }); + + it('should get a secret', () => { + const result = secretsService.get(testSecretName); + expect(result).toBeDefined(); + expect(result).toBe(testSecretValue); + }); + + it('should check if a secret exists', () => { + const exists = secretsService.exists(testSecretName); + expect(exists).toBe(true); + + const nonExistent = secretsService.exists('nonExistentSecret'); + expect(nonExistent).toBe(false); + }); + + it('should update a secret', () => { + const newValue = 'newValue'; + const setResult = secretsService.set(testSecretName, newValue); + expect(setResult).toBeDefined(); + expect(setResult.changes).toBe(1); + + const getResult = secretsService.get(testSecretName); + expect(getResult).toBeDefined(); + expect(getResult).toBe(newValue); + }); + + describe('secrets api', () => { + it('returns 401 if the user is not authenticated', async () => { + secretsService.set(testSecretName, testSecretValue); + const res = await request(app).get(`/${testSecretName}`); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + it('returns 404 if secret does not exist', async () => { + const res = await request(app) + .get(`/thiskeydoesnotexist`) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(404); + }); + + it('returns 204 if secret exists', async () => { + secretsService.set(testSecretName, testSecretValue); + const res = await request(app) + .get(`/${testSecretName}`) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(204); + }); + + it('returns 200 if secret was set', async () => { + secretsService.set(testSecretName, testSecretValue); + const res = await request(app) + .post(`/`) + .set('x-actual-token', 'valid-token') + .send({ name: testSecretName, value: testSecretValue }); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual({ + status: 'ok', + }); + }); + }); +}); diff --git a/packages/sync-server/src/services/secrets-service.js b/packages/sync-server/src/services/secrets-service.js new file mode 100644 index 00000000000..fb56825fe2f --- /dev/null +++ b/packages/sync-server/src/services/secrets-service.js @@ -0,0 +1,90 @@ +import createDebug from 'debug'; +import getAccountDb from '../account-db.js'; + +/** + * An enum of valid secret names. + * @readonly + * @enum {string} + */ +export const SecretName = { + gocardless_secretId: 'gocardless_secretId', + gocardless_secretKey: 'gocardless_secretKey', + simplefin_token: 'simplefin_token', + simplefin_accessKey: 'simplefin_accessKey', +}; + +class SecretsDb { + constructor() { + this.debug = createDebug('actual:secrets-db'); + this.db = null; + } + + open() { + return getAccountDb(); + } + + set(name, value) { + if (!this.db) { + this.db = this.open(); + } + + this.debug(`setting secret '${name}' to '${value}'`); + const result = this.db.mutate( + `INSERT OR REPLACE INTO secrets (name, value) VALUES (?,?)`, + [name, value], + ); + return result; + } + + get(name) { + if (!this.db) { + this.db = this.open(); + } + + this.debug(`getting secret '${name}'`); + const result = this.db.first(`SELECT value FROM secrets WHERE name =?`, [ + name, + ]); + return result; + } +} + +const secretsDb = new SecretsDb(); +const _cachedSecrets = new Map(); +/** + * A service for managing secrets stored in `secretsDb`. + */ +export const secretsService = { + /** + * Retrieves the value of a secret by name. + * @param {SecretName} name - The name of the secret to retrieve. + * @returns {string|null} The value of the secret, or null if the secret does not exist. + */ + get: (name) => { + return _cachedSecrets.get(name) ?? secretsDb.get(name)?.value ?? null; + }, + + /** + * Sets the value of a secret by name. + * @param {SecretName} name - The name of the secret to set. + * @param {string} value - The value to set for the secret. + * @returns {Object} + */ + set: (name, value) => { + const result = secretsDb.set(name, value); + + if (result.changes === 1) { + _cachedSecrets.set(name, value); + } + return result; + }, + + /** + * Determines whether a secret with the given name exists. + * @param {SecretName} name - The name of the secret to check for existence. + * @returns {boolean} True if a secret with the given name exists, false otherwise. + */ + exists: (name) => { + return Boolean(secretsService.get(name)); + }, +}; diff --git a/packages/sync-server/src/services/user-service.js b/packages/sync-server/src/services/user-service.js new file mode 100644 index 00000000000..ee353780cab --- /dev/null +++ b/packages/sync-server/src/services/user-service.js @@ -0,0 +1,261 @@ +import getAccountDb from '../account-db.js'; + +export function getUserByUsername(userName) { + if (!userName || typeof userName !== 'string') { + return null; + } + const { id } = + getAccountDb().first('SELECT id FROM users WHERE user_name = ?', [ + userName, + ]) || {}; + return id || null; +} + +export function getUserById(userId) { + if (!userId) { + return null; + } + const { id } = + getAccountDb().first('SELECT * FROM users WHERE id = ?', [userId]) || {}; + return id || null; +} + +export function getFileById(fileId) { + if (!fileId) { + return null; + } + const { id } = + getAccountDb().first('SELECT * FROM files WHERE files.id = ?', [fileId]) || + {}; + return id || null; +} + +export function validateRole(roleId) { + const possibleRoles = ['BASIC', 'ADMIN']; + return possibleRoles.some((a) => a === roleId); +} + +export function getOwnerCount() { + const { ownerCount } = getAccountDb().first( + `SELECT count(*) as ownerCount FROM users WHERE users.user_name <> '' and users.owner = 1`, + ) || { ownerCount: 0 }; + return ownerCount; +} + +export function getOwnerId() { + const { id } = + getAccountDb().first( + `SELECT users.id FROM users WHERE users.user_name <> '' and users.owner = 1`, + ) || {}; + return id; +} + +export function getFileOwnerId(fileId) { + const { owner } = + getAccountDb().first(`SELECT files.owner FROM files WHERE files.id = ?`, [ + fileId, + ]) || {}; + return owner; +} + +export function getAllUsers() { + return getAccountDb().all( + `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, role + FROM users + WHERE users.user_name <> ''`, + ); +} + +export function insertUser(userId, userName, displayName, enabled, role) { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, 0, ?)', + [userId, userName, displayName, enabled, role], + ); +} + +export function updateUser(userId, userName, displayName, enabled) { + if (!userId || !userName) { + throw new Error('Invalid user parameters'); + } + try { + getAccountDb().mutate( + 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', + [userName, displayName, enabled, userId], + ); + } catch (error) { + throw new Error(`Failed to update user: ${error.message}`); + } +} + +export function updateUserWithRole( + userId, + userName, + displayName, + enabled, + roleId, +) { + getAccountDb().transaction(() => { + getAccountDb().mutate( + 'UPDATE users SET user_name = ?, display_name = ?, enabled = ?, role = ? WHERE id = ?', + [userName, displayName, enabled, roleId, userId], + ); + }); +} + +export function deleteUser(userId) { + return getAccountDb().mutate('DELETE FROM users WHERE id = ? and owner = 0', [ + userId, + ]).changes; +} +export function deleteUserAccess(userId) { + try { + return getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [ + userId, + ]).changes; + } catch (error) { + throw new Error(`Failed to delete user access: ${error.message}`); + } +} + +export function transferAllFilesFromUser(ownerId, oldUserId) { + if (!ownerId || !oldUserId) { + throw new Error('Invalid user IDs'); + } + try { + getAccountDb().transaction(() => { + const ownerExists = getUserById(ownerId); + if (!ownerExists) { + throw new Error('New owner not found'); + } + getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ + ownerId, + oldUserId, + ]); + }); + } catch (error) { + throw new Error(`Failed to transfer files: ${error.message}`); + } +} + +export function updateFileOwner(ownerId, fileId) { + if (!ownerId || !fileId) { + throw new Error('Invalid parameters'); + } + try { + const result = getAccountDb().mutate( + 'UPDATE files set owner = ? WHERE id = ?', + [ownerId, fileId], + ); + if (result.changes === 0) { + throw new Error('File not found'); + } + } catch (error) { + throw new Error(`Failed to update file owner: ${error.message}`); + } +} + +export function getUserAccess(fileId, userId, isAdmin) { + return getAccountDb().all( + `SELECT users.id as userId, user_name as userName, files.owner, display_name as displayName + FROM users + JOIN user_access ON user_access.user_id = users.id + JOIN files ON files.id = user_access.file_id + WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, + [fileId, userId, isAdmin ? 1 : 0], + ); +} + +export function countUserAccess(fileId, userId) { + const { accessCount } = + getAccountDb().first( + `SELECT COUNT(*) as accessCount + FROM files + WHERE files.id = ? AND (files.owner = ? OR EXISTS ( + SELECT 1 FROM user_access + WHERE user_access.user_id = ? AND user_access.file_id = ?) + )`, + [fileId, userId, userId, fileId], + ) || {}; + + return accessCount || 0; +} + +export function checkFilePermission(fileId, userId) { + return ( + getAccountDb().first( + `SELECT 1 as granted + FROM files + WHERE files.id = ? and (files.owner = ?)`, + [fileId, userId], + ) || { granted: 0 } + ); +} + +export function addUserAccess(userId, fileId) { + if (!userId || !fileId) { + throw new Error('Invalid parameters'); + } + try { + const userExists = getUserById(userId); + const fileExists = getFileById(fileId); + if (!userExists || !fileExists) { + throw new Error('User or file not found'); + } + getAccountDb().mutate( + 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', + [userId, fileId], + ); + } catch (error) { + if (error.message.includes('UNIQUE constraint')) { + throw new Error('Access already exists'); + } + throw new Error(`Failed to add user access: ${error.message}`); + } +} + +export function deleteUserAccessByFileId(userIds, fileId) { + if (!Array.isArray(userIds) || userIds.length === 0) { + throw new Error('The provided userIds must be a non-empty array.'); + } + + const CHUNK_SIZE = 999; + let totalChanges = 0; + + try { + getAccountDb().transaction(() => { + for (let i = 0; i < userIds.length; i += CHUNK_SIZE) { + const chunk = userIds.slice(i, i + CHUNK_SIZE); + const placeholders = chunk.map(() => '?').join(','); + + const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`; + + const result = getAccountDb().mutate(sql, [...chunk, fileId]); + totalChanges += result.changes; + } + }); + } catch (error) { + throw new Error(`Failed to delete user access: ${error.message}`); + } + + return totalChanges; +} + +export function getAllUserAccess(fileId) { + return getAccountDb().all( + `SELECT users.id as userId, user_name as userName, display_name as displayName, + CASE WHEN user_access.file_id IS NULL THEN 0 ELSE 1 END as haveAccess, + CASE WHEN files.id IS NULL THEN 0 ELSE 1 END as owner + FROM users + LEFT JOIN user_access ON user_access.file_id = ? and user_access.user_id = users.id + LEFT JOIN files ON files.id = ? and files.owner = users.id + WHERE users.enabled = 1 AND users.user_name <> ''`, + [fileId, fileId], + ); +} + +export function getOpenIDConfig() { + return ( + getAccountDb().first(`SELECT * FROM auth WHERE method = ?`, ['openid']) || + null + ); +} diff --git a/packages/sync-server/src/sql/messages.sql b/packages/sync-server/src/sql/messages.sql new file mode 100644 index 00000000000..64d35cb7ec4 --- /dev/null +++ b/packages/sync-server/src/sql/messages.sql @@ -0,0 +1,9 @@ + +CREATE TABLE messages_binary + (timestamp TEXT PRIMARY KEY, + is_encrypted BOOLEAN, + content bytea); + +CREATE TABLE messages_merkles + (id INTEGER PRIMARY KEY, + merkle TEXT); diff --git a/packages/sync-server/src/sync-simple.js b/packages/sync-server/src/sync-simple.js new file mode 100644 index 00000000000..af0f99f93e1 --- /dev/null +++ b/packages/sync-server/src/sync-simple.js @@ -0,0 +1,95 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import openDatabase from './db.js'; +import { getPathForGroupFile } from './util/paths.js'; + +import { sqlDir } from './load-config.js'; + +import { merkle, SyncProtoBuf, Timestamp } from '@actual-app/crdt'; + +function getGroupDb(groupId) { + let path = getPathForGroupFile(groupId); + let needsInit = !existsSync(path); + + let db = openDatabase(path); + + if (needsInit) { + let sql = readFileSync(join(sqlDir, 'messages.sql'), 'utf8'); + db.exec(sql); + } + + return db; +} + +function addMessages(db, messages) { + let returnValue; + db.transaction(() => { + let trie = getMerkle(db); + + if (messages.length > 0) { + for (let msg of messages) { + let info = db.mutate( + `INSERT OR IGNORE INTO messages_binary (timestamp, is_encrypted, content) + VALUES (?, ?, ?)`, + [ + msg.getTimestamp(), + msg.getIsencrypted() ? 1 : 0, + Buffer.from(msg.getContent()), + ], + ); + + if (info.changes > 0) { + trie = merkle.insert(trie, Timestamp.parse(msg.getTimestamp())); + } + } + } + + trie = merkle.prune(trie); + + db.mutate( + 'INSERT INTO messages_merkles (id, merkle) VALUES (1, ?) ON CONFLICT (id) DO UPDATE SET merkle = ?', + [JSON.stringify(trie), JSON.stringify(trie)], + ); + + returnValue = trie; + }); + + return returnValue; +} + +function getMerkle(db) { + let rows = db.all('SELECT * FROM messages_merkles'); + + if (rows.length > 0) { + return JSON.parse(rows[0].merkle); + } else { + // No merkle trie exists yet (first sync of the app), so create a + // default one. + return {}; + } +} + +export function sync(messages, since, groupId) { + let db = getGroupDb(groupId); + let newMessages = db.all( + `SELECT * FROM messages_binary + WHERE timestamp > ? + ORDER BY timestamp`, + [since], + ); + + let trie = addMessages(db, messages); + + db.close(); + + return { + trie, + newMessages: newMessages.map((msg) => { + const envelopePb = new SyncProtoBuf.MessageEnvelope(); + envelopePb.setTimestamp(msg.timestamp); + envelopePb.setIsencrypted(msg.is_encrypted); + envelopePb.setContent(msg.content); + return envelopePb; + }), + }; +} diff --git a/packages/sync-server/src/util/hash.js b/packages/sync-server/src/util/hash.js new file mode 100644 index 00000000000..e8f14f3e4e0 --- /dev/null +++ b/packages/sync-server/src/util/hash.js @@ -0,0 +1,5 @@ +import crypto from 'crypto'; + +export async function sha256String(str) { + return crypto.createHash('sha256').update(str).digest('base64'); +} diff --git a/packages/sync-server/src/util/middlewares.js b/packages/sync-server/src/util/middlewares.js new file mode 100644 index 00000000000..f8d6b4f1897 --- /dev/null +++ b/packages/sync-server/src/util/middlewares.js @@ -0,0 +1,58 @@ +import validateSession from './validate-user.js'; + +import * as winston from 'winston'; +import * as expressWinston from 'express-winston'; + +/** + * @param {Error} err + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next + */ +async function errorMiddleware(err, req, res, next) { + if (res.headersSent) { + // If you call next() with an error after you have started writing the response + // (for example, if you encounter an error while streaming the response + // to the client), the Express default error handler closes + // the connection and fails the request. + + // So when you add a custom error handler, you must delegate + // to the default Express error handler, when the headers + // have already been sent to the client + // Source: https://expressjs.com/en/guide/error-handling.html + return next(err); + } + console.log(`Error on endpoint ${req.url}`, err.message, err.stack); + res.status(500).send({ status: 'error', reason: 'internal-error' }); +} + +/** + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next + */ +const validateSessionMiddleware = async (req, res, next) => { + let session = await validateSession(req, res); + if (!session) { + return; + } + + res.locals = session; + next(); +}; + +const requestLoggerMiddleware = expressWinston.logger({ + transports: [new winston.transports.Console()], + format: winston.format.combine( + winston.format.colorize(), + winston.format.timestamp(), + winston.format.printf((args) => { + const { timestamp, level, meta } = args; + const { res, req } = meta; + + return `${timestamp} ${level}: ${req.method} ${res.statusCode} ${req.url}`; + }), + ), +}); + +export { validateSessionMiddleware, errorMiddleware, requestLoggerMiddleware }; diff --git a/packages/sync-server/src/util/paths.js b/packages/sync-server/src/util/paths.js new file mode 100644 index 00000000000..971bcacd9f5 --- /dev/null +++ b/packages/sync-server/src/util/paths.js @@ -0,0 +1,12 @@ +import { join } from 'node:path'; +import config from '../load-config.js'; + +/** @param {string} fileId */ +export function getPathForUserFile(fileId) { + return join(config.userFiles, `file-${fileId}.blob`); +} + +/** @param {string} groupId */ +export function getPathForGroupFile(groupId) { + return join(config.userFiles, `group-${groupId}.sqlite`); +} diff --git a/packages/sync-server/src/util/payee-name.js b/packages/sync-server/src/util/payee-name.js new file mode 100644 index 00000000000..1967aa49504 --- /dev/null +++ b/packages/sync-server/src/util/payee-name.js @@ -0,0 +1,45 @@ +import { title } from './title/index.js'; + +function formatPayeeIban(iban) { + return '(' + iban.slice(0, 4) + ' XXX ' + iban.slice(-4) + ')'; +} + +export const formatPayeeName = (trans) => { + const amount = trans.transactionAmount.amount; + const nameParts = []; + + // get the correct name and account fields for the transaction amount + let name; + let account; + if (amount > 0 || Object.is(Number(amount), 0)) { + name = trans.debtorName; + account = trans.debtorAccount; + } else { + name = trans.creditorName; + account = trans.creditorAccount; + } + + // use the correct name field if it was found + // if not, use whatever we can find + + // if the primary name option is set, prevent the account from falling back + account = name ? account : trans.debtorAccount || trans.creditorAccount; + + name = + name || + trans.debtorName || + trans.creditorName || + trans.remittanceInformationUnstructured || + (trans.remittanceInformationUnstructuredArray || []).join(', ') || + trans.additionalInformation; + + if (name) { + nameParts.push(title(name)); + } + + if (account && account.iban) { + nameParts.push(formatPayeeIban(account.iban)); + } + + return nameParts.join(' '); +}; diff --git a/packages/sync-server/src/util/prompt.js b/packages/sync-server/src/util/prompt.js new file mode 100644 index 00000000000..f66f0f7605c --- /dev/null +++ b/packages/sync-server/src/util/prompt.js @@ -0,0 +1,88 @@ +import { createInterface, cursorTo } from 'node:readline'; + +export async function prompt(message) { + let rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + let promise = new Promise((resolve) => { + rl.question(message, (answer) => { + resolve(answer); + rl.close(); + }); + }); + + let answer = await promise; + + return answer; +} + +export async function promptPassword() { + let password = await askForPassword('Enter a password, then press enter: '); + + if (password === '') { + console.log('Password cannot be empty.'); + return promptPassword(); + } + + let password2 = await askForPassword( + 'Enter the password again, then press enter: ', + ); + + if (password !== password2) { + console.log('Passwords do not match.'); + return promptPassword(); + } + + return password; +} + +async function askForPassword(prompt) { + let dataListener, endListener; + + let promise = new Promise((resolve) => { + let result = ''; + process.stdout.write(prompt); + process.stdin.setRawMode(true); + process.stdin.resume(); + dataListener = (key) => { + switch (key[0]) { + case 0x03: // ^C + process.exit(); + break; + case 0x0d: // Enter + process.stdin.setRawMode(false); + process.stdin.pause(); + resolve(result); + break; + case 0x7f: // Backspace + case 0x08: // Delete + if (result) { + result = result.slice(0, -1); + cursorTo(process.stdout, prompt.length + result.length); + process.stdout.write(' '); + cursorTo(process.stdout, prompt.length + result.length); + } + break; + default: + result += key; + process.stdout.write('*'); + break; + } + }; + process.stdin.on('data', dataListener); + + endListener = () => resolve(result); + process.stdin.on('end', endListener); + }); + + let answer = await promise; + + process.stdin.off('data', dataListener); + process.stdin.off('end', endListener); + + process.stdout.write('\n'); + + return answer; +} diff --git a/packages/sync-server/src/util/title/index.js b/packages/sync-server/src/util/title/index.js new file mode 100644 index 00000000000..a77f48a2583 --- /dev/null +++ b/packages/sync-server/src/util/title/index.js @@ -0,0 +1,59 @@ +// Utilities +import { lowerCaseSet } from './lower-case.js'; +import { specials } from './specials.js'; + +const character = + '[0-9\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376-\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0621-\u064A\u066E-\u066F\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4-\u07F5\u07FA\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0972\u097B-\u097F\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58-\u0C59\u0C60-\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D3D\u0D60-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8B\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065-\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10D0-\u10FA\u10FC\u1100-\u1159\u115F-\u11A2\u11A8-\u11F9\u1200-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u1676\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19A9\u19C1-\u19C7\u1A00-\u1A16\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE-\u1BAF\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2C6F\u2C71-\u2C7D\u2C80-\u2CE4\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400\u4DB5\u4E00\u9FC3\uA000-\uA48C\uA500-\uA60C\uA610-\uA61F\uA62A-\uA62B\uA640-\uA65F\uA662-\uA66E\uA67F-\uA697\uA717-\uA71F\uA722-\uA788\uA78B-\uA78C\uA7FB-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA90A-\uA925\uA930-\uA946\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAC00\uD7A3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]'; +const regex = new RegExp( + `(?:(?:(\\s?(?:^|[.\\(\\)!?;:"-])\\s*)(${character}))|(${character}))(${character}*[’']*${character}*)`, + 'g', +); + +const convertToRegExp = (specials) => + specials.map((s) => [new RegExp(`\\b${s}\\b`, 'gi'), s]); + +function parseMatch(match) { + const firstCharacter = match[0]; + + // test first character + if (/\s/.test(firstCharacter)) { + // if whitespace - trim and return + return match.substr(1); + } + if (/[()]/.test(firstCharacter)) { + // if parens - this shouldn't be replaced + return null; + } + + return match; +} + +export function title(str, options = { special: undefined }) { + str = str + .toLowerCase() + .replace(regex, (m, lead = '', forced, lower, rest) => { + const parsedMatch = parseMatch(m); + if (!parsedMatch) { + return m; + } + if (!forced) { + const fullLower = lower + rest; + + if (lowerCaseSet.has(fullLower)) { + return parsedMatch; + } + } + + return lead + (lower || forced).toUpperCase() + rest; + }); + + const customSpecials = options.special || []; + const replace = [...specials, ...customSpecials]; + const replaceRegExp = convertToRegExp(replace); + + replaceRegExp.forEach(([pattern, s]) => { + str = str.replace(pattern, s); + }); + + return str; +} diff --git a/packages/sync-server/src/util/title/lower-case.js b/packages/sync-server/src/util/title/lower-case.js new file mode 100644 index 00000000000..eaecf439e77 --- /dev/null +++ b/packages/sync-server/src/util/title/lower-case.js @@ -0,0 +1,93 @@ +const conjunctions = [ + 'for', // + 'and', + 'nor', + 'but', + 'or', + 'yet', + 'so', +]; + +const articles = [ + 'a', // + 'an', + 'the', +]; + +const prepositions = [ + 'aboard', + 'about', + 'above', + 'across', + 'after', + 'against', + 'along', + 'amid', + 'among', + 'anti', + 'around', + 'as', + 'at', + 'before', + 'behind', + 'below', + 'beneath', + 'beside', + 'besides', + 'between', + 'beyond', + 'but', + 'by', + 'concerning', + 'considering', + 'despite', + 'down', + 'during', + 'except', + 'excepting', + 'excluding', + 'following', + 'for', + 'from', + 'in', + 'inside', + 'into', + 'like', + 'minus', + 'near', + 'of', + 'off', + 'on', + 'onto', + 'opposite', + 'over', + 'past', + 'per', + 'plus', + 'regarding', + 'round', + 'save', + 'since', + 'than', + 'through', + 'to', + 'toward', + 'towards', + 'under', + 'underneath', + 'unlike', + 'until', + 'up', + 'upon', + 'versus', + 'via', + 'with', + 'within', + 'without', +]; + +export const lowerCaseSet = new Set([ + ...conjunctions, + ...articles, + ...prepositions, +]); diff --git a/packages/sync-server/src/util/title/specials.js b/packages/sync-server/src/util/title/specials.js new file mode 100644 index 00000000000..bf66c498c45 --- /dev/null +++ b/packages/sync-server/src/util/title/specials.js @@ -0,0 +1,21 @@ +export const specials = [ + 'CLI', + 'API', + 'HTTP', + 'HTTPS', + 'JSX', + 'DNS', + 'URL', + 'CI', + 'CDN', + 'GitHub', + 'CSS', + 'JS', + 'JavaScript', + 'TypeScript', + 'HTML', + 'WordPress', + 'JavaScript', + 'Next.js', + 'Node.js', +]; diff --git a/packages/sync-server/src/util/validate-user.js b/packages/sync-server/src/util/validate-user.js new file mode 100644 index 00000000000..a84389e6f64 --- /dev/null +++ b/packages/sync-server/src/util/validate-user.js @@ -0,0 +1,68 @@ +import config from '../load-config.js'; +import proxyaddr from 'proxy-addr'; +import ipaddr from 'ipaddr.js'; +import { getSession } from '../account-db.js'; + +export const TOKEN_EXPIRATION_NEVER = -1; +const MS_PER_SECOND = 1000; + +/** + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +export default function validateSession(req, res) { + let { token } = req.body || {}; + + if (!token) { + token = req.headers['x-actual-token']; + } + + let session = getSession(token); + + if (!session) { + res.status(401); + res.send({ + status: 'error', + reason: 'unauthorized', + details: 'token-not-found', + }); + return null; + } + + if ( + session.expires_at !== TOKEN_EXPIRATION_NEVER && + session.expires_at * MS_PER_SECOND <= Date.now() + ) { + res.status(401); + res.send({ + status: 'error', + reason: 'token-expired', + }); + return null; + } + + return session; +} + +export function validateAuthHeader(req) { + if (config.trustedProxies.length == 0) { + return true; + } + + let sender = proxyaddr(req, 'uniquelocal'); + let sender_ip = ipaddr.process(sender); + const rangeList = { + allowed_ips: config.trustedProxies.map((q) => ipaddr.parseCIDR(q)), + }; + /* eslint-disable @typescript-eslint/ban-ts-comment */ + // @ts-ignore : there is an error in the ts definition for the function, but this is valid + var matched = ipaddr.subnetMatch(sender_ip, rangeList, 'fail'); + /* eslint-enable @typescript-eslint/ban-ts-comment */ + if (matched == 'allowed_ips') { + console.info(`Header Auth Login permitted from ${sender}`); + return true; + } else { + console.warn(`Header Auth Login attempted from ${sender}`); + return false; + } +} diff --git a/packages/sync-server/tsconfig.json b/packages/sync-server/tsconfig.json new file mode 100644 index 00000000000..cb09bd65f4b --- /dev/null +++ b/packages/sync-server/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + // DOM for URL global in Node 16+ + "lib": ["ES2021"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "preserve", + // Check JS files too + "allowJs": true, + "checkJs": true, + "moduleResolution": "node16", + "module": "node16", + "outDir": "build" + }, + "include": ["src/**/*.js", "types/global.d.ts"], + "exclude": ["node_modules", "build", "./app-plaid.js", "coverage"], +} diff --git a/packages/sync-server/upcoming-release-notes/479.md b/packages/sync-server/upcoming-release-notes/479.md new file mode 100644 index 00000000000..17fd70b70eb --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/479.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [rare-magma] +--- + +Updates the docker images base version and set node_env env variable to production diff --git a/packages/sync-server/upcoming-release-notes/484.md b/packages/sync-server/upcoming-release-notes/484.md new file mode 100644 index 00000000000..2d7c9e07c43 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/484.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [matt-fidd] +--- + +Add support for `1822-DIREKT-HELADEF1822` transaction information diff --git a/packages/sync-server/upcoming-release-notes/485.md b/packages/sync-server/upcoming-release-notes/485.md new file mode 100644 index 00000000000..3fd1ff078df --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/485.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [psybers] +--- + +Check if SimpleFIN accessKey is in the correct format. diff --git a/packages/sync-server/upcoming-release-notes/490.md b/packages/sync-server/upcoming-release-notes/490.md new file mode 100644 index 00000000000..850e6140aea --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/490.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [dmednis] +--- + +Add support for "SWEDBANK_HABALV22" transaction date \ No newline at end of file diff --git a/packages/sync-server/upcoming-release-notes/493.md b/packages/sync-server/upcoming-release-notes/493.md new file mode 100644 index 00000000000..b8767a9a774 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/493.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [matt-fidd] +--- + +GoCardless: `ING_PL_INGBPLPW` should prefer valueDate over bookingDate diff --git a/packages/sync-server/upcoming-release-notes/494.md b/packages/sync-server/upcoming-release-notes/494.md new file mode 100644 index 00000000000..9efdee05c2a --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/494.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [matt-fidd] +--- + +Prefer using the SimpleFin pending flag to set cleared status diff --git a/packages/sync-server/upcoming-release-notes/497.md b/packages/sync-server/upcoming-release-notes/497.md new file mode 100644 index 00000000000..0836318c8a0 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/497.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [dmednis] +--- + +Improve support for "SWEDBANK_HABALV22" transaction date & enrich creditor name for pending transactions \ No newline at end of file diff --git a/packages/sync-server/upcoming-release-notes/498.md b/packages/sync-server/upcoming-release-notes/498.md new file mode 100644 index 00000000000..e1b8c807de7 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/498.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [apilat, lelemm] +--- + +Add support for authentication using OpenID Connect. diff --git a/packages/sync-server/upcoming-release-notes/504.md b/packages/sync-server/upcoming-release-notes/504.md new file mode 100644 index 00000000000..5f3364eba19 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/504.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [matt-fidd] +--- + +Fix bug in batch SimpleFIN startDate logic diff --git a/packages/sync-server/upcoming-release-notes/506.md b/packages/sync-server/upcoming-release-notes/506.md new file mode 100644 index 00000000000..a863badfbeb --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/506.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [kyrias] +--- + +Add GoCardless integration for ENTERCARD_SWEDNOKK diff --git a/packages/sync-server/upcoming-release-notes/509.md b/packages/sync-server/upcoming-release-notes/509.md new file mode 100644 index 00000000000..91e2afe4714 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/509.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [matt-fidd] +--- + +Add more logging for GoCardless rate limit information diff --git a/packages/sync-server/upcoming-release-notes/510.md b/packages/sync-server/upcoming-release-notes/510.md new file mode 100644 index 00000000000..9f91704e4bb --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/510.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [matt-fidd] +--- + +GoCardless: `ISYBANK_ITBBITMM` should prefer valueDate over bookingDate diff --git a/packages/sync-server/upcoming-release-notes/512.md b/packages/sync-server/upcoming-release-notes/512.md new file mode 100644 index 00000000000..4a047ac86b1 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/512.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [guglicap] +--- + +Enhances Hype Bank transaction info parsing diff --git a/packages/sync-server/upcoming-release-notes/513.md b/packages/sync-server/upcoming-release-notes/513.md new file mode 100644 index 00000000000..8f19814e137 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/513.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [nsulzer] +--- + +Add GoCardless integration for ABNAMRO_ABNANL2A \ No newline at end of file diff --git a/packages/sync-server/upcoming-release-notes/518.md b/packages/sync-server/upcoming-release-notes/518.md new file mode 100644 index 00000000000..b69ddb5c0b3 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/518.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [Sthaagg] +--- + +Add support for "FORTUNEO_FTNOFRP1XXX" to BANKS_WITH_LIMITED_HISTORY diff --git a/packages/sync-server/upcoming-release-notes/523.md b/packages/sync-server/upcoming-release-notes/523.md new file mode 100644 index 00000000000..b1e4935b975 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/523.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [lelemm] +--- + +Fixed OpenID authentication bug for Electron diff --git a/packages/sync-server/upcoming-release-notes/524.md b/packages/sync-server/upcoming-release-notes/524.md new file mode 100644 index 00000000000..282193932b0 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/524.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [FliegendeWurst] +--- + +Support for Node v22+, by upgrading the better-sqlite3 dependency. diff --git a/packages/sync-server/upcoming-release-notes/526.md b/packages/sync-server/upcoming-release-notes/526.md new file mode 100644 index 00000000000..7870ad79ae3 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/526.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [lelemm] +--- + +Better invalid password message when disabling openid diff --git a/packages/sync-server/upcoming-release-notes/527.md b/packages/sync-server/upcoming-release-notes/527.md new file mode 100644 index 00000000000..575328f072f --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/527.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [lelemm] +--- + +Commands to enable/disable OpenID from console. Also, enabling to login with oauth2 (for github). diff --git a/packages/sync-server/upcoming-release-notes/531.md b/packages/sync-server/upcoming-release-notes/531.md new file mode 100644 index 00000000000..bb2262bc503 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/531.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [DennaGherlyn] +--- + +Add GoCardless formatter for `SSK_DUSSELDORF_DUSSDEDDXXX` Stadtsparkasse Düsseldorf (Germany) diff --git a/packages/sync-server/upcoming-release-notes/533.md b/packages/sync-server/upcoming-release-notes/533.md new file mode 100644 index 00000000000..93f58649de6 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/533.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [robxgd] +--- + +Fixed issue when no payeename is given for KBC transaction \ No newline at end of file diff --git a/packages/sync-server/upcoming-release-notes/534.md b/packages/sync-server/upcoming-release-notes/534.md new file mode 100644 index 00000000000..3d03094a666 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/534.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [UnderKoen] +--- + +Make Google Pay transactions work for ABNAMRO_ABNANL2A diff --git a/packages/sync-server/upcoming-release-notes/535.md b/packages/sync-server/upcoming-release-notes/535.md new file mode 100644 index 00000000000..95df9343005 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/535.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [spideraxal] +--- + +Add corner case transaction for ING Bank Romania diff --git a/packages/sync-server/upcoming-release-notes/538.md b/packages/sync-server/upcoming-release-notes/538.md new file mode 100644 index 00000000000..fd2b853516e --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/538.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [lnagel] +--- + +Fix WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match in Dockerfiles diff --git a/packages/sync-server/upcoming-release-notes/539.md b/packages/sync-server/upcoming-release-notes/539.md new file mode 100644 index 00000000000..28b6968b7ed --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/539.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [MatissJanis] +--- + +Add GoCardless formatter for `BANK_OF_IRELAND_B365_BOFIIE2D` Bank of Ireland. diff --git a/packages/sync-server/upcoming-release-notes/541.md b/packages/sync-server/upcoming-release-notes/541.md new file mode 100644 index 00000000000..56a0e2b54e2 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/541.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [matt-fidd] +--- + +Standardize GoCardless bank handlers diff --git a/packages/sync-server/upcoming-release-notes/README.md b/packages/sync-server/upcoming-release-notes/README.md new file mode 100644 index 00000000000..988027c3dd3 --- /dev/null +++ b/packages/sync-server/upcoming-release-notes/README.md @@ -0,0 +1 @@ +See the [Writing Good Release Notes](https://actualbudget.org/docs/contributing/#writing-good-release-notes) section of the documentation for more information on how to create a release notes file. diff --git a/packages/sync-server/yarn.lock b/packages/sync-server/yarn.lock new file mode 100644 index 00000000000..48c27a66ea1 --- /dev/null +++ b/packages/sync-server/yarn.lock @@ -0,0 +1,6481 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@actual-app/crdt@npm:2.1.0": + version: 2.1.0 + resolution: "@actual-app/crdt@npm:2.1.0" + dependencies: + google-protobuf: "npm:^3.12.0-rc.1" + murmurhash: "npm:^2.0.1" + uuid: "npm:^9.0.0" + checksum: 10c0/131050eb42218229eebe60a954ee55275380cff3b139a08b34e25f6056b621033f28a231ce6cc59022fdc80d475500fe70e5f134f9dc3250e37516e679922d5c + languageName: node + linkType: hard + +"@actual-app/web@npm:24.12.0": + version: 24.12.0 + resolution: "@actual-app/web@npm:24.12.0" + checksum: 10c0/865fd5898e8da6347759a65d557047b80cd0c4162601fd4f57eccd1d84289d84c9f2fe563a4b4547dc2f67353042fb94a6e9c0cb13cacfda0547f84aaba73ef6 + languageName: node + linkType: hard + +"@ampproject/remapping@npm:^2.2.0": + version: 2.3.0 + resolution: "@ampproject/remapping@npm:2.3.0" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/81d63cca5443e0f0c72ae18b544cc28c7c0ec2cea46e7cb888bb0e0f411a1191d0d6b7af798d54e30777d8d1488b2ec0732aac2be342d3d7d3ffd271c6f489ed + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/code-frame@npm:7.24.7" + dependencies: + "@babel/highlight": "npm:^7.24.7" + picocolors: "npm:^1.0.0" + checksum: 10c0/ab0af539473a9f5aeaac7047e377cb4f4edd255a81d84a76058595f8540784cc3fbe8acf73f1e073981104562490aabfb23008cd66dc677a456a4ed5390fdde6 + languageName: node + linkType: hard + +"@babel/compat-data@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/compat-data@npm:7.24.7" + checksum: 10c0/dcd93a5632b04536498fbe2be5af1057f635fd7f7090483d8e797878559037e5130b26862ceb359acbae93ed27e076d395ddb4663db6b28a665756ffd02d324f + languageName: node + linkType: hard + +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9": + version: 7.24.7 + resolution: "@babel/core@npm:7.24.7" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.24.7" + "@babel/helper-compilation-targets": "npm:^7.24.7" + "@babel/helper-module-transforms": "npm:^7.24.7" + "@babel/helpers": "npm:^7.24.7" + "@babel/parser": "npm:^7.24.7" + "@babel/template": "npm:^7.24.7" + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10c0/4004ba454d3c20a46ea66264e06c15b82e9f6bdc35f88819907d24620da70dbf896abac1cb4cc4b6bb8642969e45f4d808497c9054a1388a386cf8c12e9b9e0d + languageName: node + linkType: hard + +"@babel/generator@npm:^7.24.7, @babel/generator@npm:^7.7.2": + version: 7.24.7 + resolution: "@babel/generator@npm:7.24.7" + dependencies: + "@babel/types": "npm:^7.24.7" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^2.5.1" + checksum: 10c0/06b1f3350baf527a3309e50ffd7065f7aee04dd06e1e7db794ddfde7fe9d81f28df64edd587173f8f9295496a7ddb74b9a185d4bf4de7bb619e6d4ec45c8fd35 + languageName: node + linkType: hard + +"@babel/helper-annotate-as-pure@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-annotate-as-pure@npm:7.24.7" + dependencies: + "@babel/types": "npm:^7.24.7" + checksum: 10c0/4679f7df4dffd5b3e26083ae65228116c3da34c3fff2c11ae11b259a61baec440f51e30fd236f7a0435b9d471acd93d0bc5a95df8213cbf02b1e083503d81b9a + languageName: node + linkType: hard + +"@babel/helper-compilation-targets@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-compilation-targets@npm:7.24.7" + dependencies: + "@babel/compat-data": "npm:^7.24.7" + "@babel/helper-validator-option": "npm:^7.24.7" + browserslist: "npm:^4.22.2" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10c0/1d580a9bcacefe65e6bf02ba1dafd7ab278269fef45b5e281d8354d95c53031e019890464e7f9351898c01502dd2e633184eb0bcda49ed2ecd538675ce310f51 + languageName: node + linkType: hard + +"@babel/helper-create-class-features-plugin@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-create-class-features-plugin@npm:7.24.7" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-member-expression-to-functions": "npm:^7.24.7" + "@babel/helper-optimise-call-expression": "npm:^7.24.7" + "@babel/helper-replace-supers": "npm:^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/6b7b47d70b41c00f39f86790cff67acf2bce0289d52a7c182b28e797f4e0e6d69027e3d06eccf1d54dddc2e5dde1df663bb1932437e5f447aeb8635d8d64a6ab + languageName: node + linkType: hard + +"@babel/helper-environment-visitor@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-environment-visitor@npm:7.24.7" + dependencies: + "@babel/types": "npm:^7.24.7" + checksum: 10c0/36ece78882b5960e2d26abf13cf15ff5689bf7c325b10a2895a74a499e712de0d305f8d78bb382dd3c05cfba7e47ec98fe28aab5674243e0625cd38438dd0b2d + languageName: node + linkType: hard + +"@babel/helper-function-name@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-function-name@npm:7.24.7" + dependencies: + "@babel/template": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10c0/e5e41e6cf86bd0f8bf272cbb6e7c5ee0f3e9660414174435a46653efba4f2479ce03ce04abff2aa2ef9359cf057c79c06cb7b134a565ad9c0e8a50dcdc3b43c4 + languageName: node + linkType: hard + +"@babel/helper-hoist-variables@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-hoist-variables@npm:7.24.7" + dependencies: + "@babel/types": "npm:^7.24.7" + checksum: 10c0/19ee37563bbd1219f9d98991ad0e9abef77803ee5945fd85aa7aa62a67c69efca9a801696a1b58dda27f211e878b3327789e6fd2a6f6c725ccefe36774b5ce95 + languageName: node + linkType: hard + +"@babel/helper-member-expression-to-functions@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-member-expression-to-functions@npm:7.24.7" + dependencies: + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10c0/9638c1d33cf6aba028461ccd3db6061c76ff863ca0d5013dd9a088bf841f2f77c46956493f9da18355c16759449d23b74cc1de4da357ade5c5c34c858f840f0a + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-module-imports@npm:7.24.7" + dependencies: + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10c0/97c57db6c3eeaea31564286e328a9fb52b0313c5cfcc7eee4bc226aebcf0418ea5b6fe78673c0e4a774512ec6c86e309d0f326e99d2b37bfc16a25a032498af0 + languageName: node + linkType: hard + +"@babel/helper-module-transforms@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-module-transforms@npm:7.24.7" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-module-imports": "npm:^7.24.7" + "@babel/helper-simple-access": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + "@babel/helper-validator-identifier": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/4f311755fcc3b4cbdb689386309cdb349cf0575a938f0b9ab5d678e1a81bbb265aa34ad93174838245f2ac7ff6d5ddbd0104638a75e4e961958ed514355687b6 + languageName: node + linkType: hard + +"@babel/helper-optimise-call-expression@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-optimise-call-expression@npm:7.24.7" + dependencies: + "@babel/types": "npm:^7.24.7" + checksum: 10c0/ca6a9884705dea5c95a8b3ce132d1e3f2ae951ff74987d400d1d9c215dae9c0f9e29924d8f8e131e116533d182675bc261927be72f6a9a2968eaeeaa51eb1d0f + languageName: node + linkType: hard + +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.24.7, @babel/helper-plugin-utils@npm:^7.8.0": + version: 7.24.7 + resolution: "@babel/helper-plugin-utils@npm:7.24.7" + checksum: 10c0/c3d38cd9b3520757bb4a279255cc3f956fc0ac1c193964bd0816ebd5c86e30710be8e35252227e0c9d9e0f4f56d9b5f916537f2bc588084b0988b4787a967d31 + languageName: node + linkType: hard + +"@babel/helper-replace-supers@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-replace-supers@npm:7.24.7" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-member-expression-to-functions": "npm:^7.24.7" + "@babel/helper-optimise-call-expression": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/0e133bb03371dee78e519c334a09c08e1493103a239d9628db0132dfaac3fc16380479ca3c590d278a9b71b624030a338c18ebbfe6d430ebb2e4653775c4b3e3 + languageName: node + linkType: hard + +"@babel/helper-simple-access@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-simple-access@npm:7.24.7" + dependencies: + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10c0/7230e419d59a85f93153415100a5faff23c133d7442c19e0cd070da1784d13cd29096ee6c5a5761065c44e8164f9f80e3a518c41a0256df39e38f7ad6744fed7 + languageName: node + linkType: hard + +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.24.7" + dependencies: + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10c0/e3a9b8ac9c262ac976a1bcb5fe59694db5e6f0b4f9e7bdba5c7693b8b5e28113c23bdaa60fe8d3ec32a337091b67720b2053bcb3d5655f5406536c3d0584242b + languageName: node + linkType: hard + +"@babel/helper-split-export-declaration@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-split-export-declaration@npm:7.24.7" + dependencies: + "@babel/types": "npm:^7.24.7" + checksum: 10c0/0254577d7086bf09b01bbde98f731d4fcf4b7c3fa9634fdb87929801307c1f6202a1352e3faa5492450fa8da4420542d44de604daf540704ff349594a78184f6 + languageName: node + linkType: hard + +"@babel/helper-string-parser@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-string-parser@npm:7.24.7" + checksum: 10c0/47840c7004e735f3dc93939c77b099bb41a64bf3dda0cae62f60e6f74a5ff80b63e9b7cf77b5ec25a324516381fc994e1f62f922533236a8e3a6af57decb5e1e + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-validator-identifier@npm:7.24.7" + checksum: 10c0/87ad608694c9477814093ed5b5c080c2e06d44cb1924ae8320474a74415241223cc2a725eea2640dd783ff1e3390e5f95eede978bc540e870053152e58f1d651 + languageName: node + linkType: hard + +"@babel/helper-validator-option@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-validator-option@npm:7.24.7" + checksum: 10c0/21aea2b7bc5cc8ddfb828741d5c8116a84cbc35b4a3184ec53124f08e09746f1f67a6f9217850188995ca86059a7942e36d8965a6730784901def777b7e8a436 + languageName: node + linkType: hard + +"@babel/helpers@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helpers@npm:7.24.7" + dependencies: + "@babel/template": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10c0/aa8e230f6668773e17e141dbcab63e935c514b4b0bf1fed04d2eaefda17df68e16b61a56573f7f1d4d1e605ce6cc162b5f7e9fdf159fde1fd9b77c920ae47d27 + languageName: node + linkType: hard + +"@babel/highlight@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/highlight@npm:7.24.7" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.24.7" + chalk: "npm:^2.4.2" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10c0/674334c571d2bb9d1c89bdd87566383f59231e16bcdcf5bb7835babdf03c9ae585ca0887a7b25bdf78f303984af028df52831c7989fecebb5101cc132da9393a + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/parser@npm:7.24.7" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/8b244756872185a1c6f14b979b3535e682ff08cb5a2a5fd97cc36c017c7ef431ba76439e95e419d43000c5b07720495b00cf29a7f0d9a483643d08802b58819b + languageName: node + linkType: hard + +"@babel/plugin-syntax-async-generators@npm:^7.8.4": + version: 7.8.4 + resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/d13efb282838481348c71073b6be6245b35d4f2f964a8f71e4174f235009f929ef7613df25f8d2338e2d3e44bc4265a9f8638c6aaa136d7a61fe95985f9725c8 + languageName: node + linkType: hard + +"@babel/plugin-syntax-bigint@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-bigint@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/686891b81af2bc74c39013655da368a480f17dd237bf9fbc32048e5865cb706d5a8f65438030da535b332b1d6b22feba336da8fa931f663b6b34e13147d12dde + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-properties@npm:^7.8.3": + version: 7.12.13 + resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.12.13" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/95168fa186416195280b1264fb18afcdcdcea780b3515537b766cb90de6ce042d42dd6a204a39002f794ae5845b02afb0fd4861a3308a861204a55e68310a120 + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-meta@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/0b08b5e4c3128523d8e346f8cfc86824f0da2697b1be12d71af50a31aff7a56ceb873ed28779121051475010c28d6146a6bfea8518b150b71eeb4e46190172ee + languageName: node + linkType: hard + +"@babel/plugin-syntax-json-strings@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/e98f31b2ec406c57757d115aac81d0336e8434101c224edd9a5c93cefa53faf63eacc69f3138960c8b25401315af03df37f68d316c151c4b933136716ed6906e + languageName: node + linkType: hard + +"@babel/plugin-syntax-jsx@npm:^7.24.7, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.24.7 + resolution: "@babel/plugin-syntax-jsx@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/f44d927a9ae8d5ef016ff5b450e1671e56629ddc12e56b938e41fd46e141170d9dfc9a53d6cb2b9a20a7dd266a938885e6a3981c60c052a2e1daed602ac80e51 + languageName: node + linkType: hard + +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/2594cfbe29411ad5bc2ad4058de7b2f6a8c5b86eda525a993959438615479e59c012c14aec979e538d60a584a1a799b60d1b8942c3b18468cb9d99b8fd34cd0b + languageName: node + linkType: hard + +"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/2024fbb1162899094cfc81152449b12bd0cc7053c6d4bda8ac2852545c87d0a851b1b72ed9560673cbf3ef6248257262c3c04aabf73117215c1b9cc7dd2542ce + languageName: node + linkType: hard + +"@babel/plugin-syntax-numeric-separator@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/c55a82b3113480942c6aa2fcbe976ff9caa74b7b1109ff4369641dfbc88d1da348aceb3c31b6ed311c84d1e7c479440b961906c735d0ab494f688bf2fd5b9bb9 + languageName: node + linkType: hard + +"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/ee1eab52ea6437e3101a0a7018b0da698545230015fc8ab129d292980ec6dff94d265e9e90070e8ae5fed42f08f1622c14c94552c77bcac784b37f503a82ff26 + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/27e2493ab67a8ea6d693af1287f7e9acec206d1213ff107a928e85e173741e1d594196f99fec50e9dde404b09164f39dec5864c767212154ffe1caa6af0bc5af + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/46edddf2faa6ebf94147b8e8540dfc60a5ab718e2de4d01b2c0bdf250a4d642c2bd47cbcbb739febcb2bf75514dbcefad3c52208787994b8d0f8822490f55e81 + languageName: node + linkType: hard + +"@babel/plugin-syntax-top-level-await@npm:^7.8.3": + version: 7.14.5 + resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/14bf6e65d5bc1231ffa9def5f0ef30b19b51c218fcecaa78cd1bdf7939dfdf23f90336080b7f5196916368e399934ce5d581492d8292b46a2fb569d8b2da106f + languageName: node + linkType: hard + +"@babel/plugin-syntax-typescript@npm:^7.24.7, @babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.24.7 + resolution: "@babel/plugin-syntax-typescript@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/cdabd2e8010fb0ad15b49c2c270efc97c4bfe109ead36c7bbcf22da7a74bc3e49702fc4f22f12d2d6049e8e22a5769258df1fd05f0420ae45e11bdd5bc07805a + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-commonjs@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.7" + dependencies: + "@babel/helper-module-transforms": "npm:^7.24.7" + "@babel/helper-plugin-utils": "npm:^7.24.7" + "@babel/helper-simple-access": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/9442292b3daf6a5076cdc3c4c32bf423bda824ccaeb0dd0dc8b3effaa1fecfcb0130ae6e647fef12a5d5ff25bcc99a0d6bfc6d24a7525345e1bcf46fcdf81752 + languageName: node + linkType: hard + +"@babel/plugin-transform-typescript@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/plugin-transform-typescript@npm:7.24.7" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-create-class-features-plugin": "npm:^7.24.7" + "@babel/helper-plugin-utils": "npm:^7.24.7" + "@babel/plugin-syntax-typescript": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/e8dacdc153a4c4599014b66eb01b94e3dc933d58d4f0cc3039c1a8f432e77b9df14f34a61964e014b975bf466f3fefd8c4768b3e887d3da1be9dc942799bdfdf + languageName: node + linkType: hard + +"@babel/preset-typescript@npm:^7.20.2": + version: 7.24.7 + resolution: "@babel/preset-typescript@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + "@babel/helper-validator-option": "npm:^7.24.7" + "@babel/plugin-syntax-jsx": "npm:^7.24.7" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.7" + "@babel/plugin-transform-typescript": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/986bc0978eedb4da33aba8e1e13a3426dd1829515313b7e8f4ba5d8c18aff1663b468939d471814e7acf4045d326ae6cff37239878d169ac3fe53a8fde71f8ee + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.21.0": + version: 7.24.7 + resolution: "@babel/runtime@npm:7.24.7" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/b6fa3ec61a53402f3c1d75f4d808f48b35e0dfae0ec8e2bb5c6fc79fb95935da75766e0ca534d0f1c84871f6ae0d2ebdd950727cfadb745a2cdbef13faef5513 + languageName: node + linkType: hard + +"@babel/template@npm:^7.24.7, @babel/template@npm:^7.3.3": + version: 7.24.7 + resolution: "@babel/template@npm:7.24.7" + dependencies: + "@babel/code-frame": "npm:^7.24.7" + "@babel/parser": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 10c0/95b0b3ee80fcef685b7f4426f5713a855ea2cd5ac4da829b213f8fb5afe48a2a14683c2ea04d446dbc7f711c33c5cd4a965ef34dcbe5bc387c9e966b67877ae3 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/traverse@npm:7.24.7" + dependencies: + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.24.7" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-hoist-variables": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + "@babel/parser": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10c0/a5135e589c3f1972b8877805f50a084a04865ccb1d68e5e1f3b94a8841b3485da4142e33413d8fd76bc0e6444531d3adf1f59f359c11ffac452b743d835068ab + languageName: node + linkType: hard + +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": + version: 7.24.7 + resolution: "@babel/types@npm:7.24.7" + dependencies: + "@babel/helper-string-parser": "npm:^7.24.7" + "@babel/helper-validator-identifier": "npm:^7.24.7" + to-fast-properties: "npm:^2.0.0" + checksum: 10c0/d9ecbfc3eb2b05fb1e6eeea546836ac30d990f395ef3fe3f75ced777a222c3cfc4489492f72e0ce3d9a5a28860a1ce5f81e66b88cf5088909068b3ff4fab72c1 + languageName: node + linkType: hard + +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 10c0/6b80ae4cb3db53f486da2dc63b6e190a74c8c3cca16bb2733f234a0b6a9382b09b146488ae08e2b22cf00f6c83e20f3e040a2f7894f05c045c946d6a090b1d52 + languageName: node + linkType: hard + +"@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0": + version: 1.6.0 + resolution: "@colors/colors@npm:1.6.0" + checksum: 10c0/9328a0778a5b0db243af54455b79a69e3fb21122d6c15ef9e9fcc94881d8d17352d8b2b2590f9bdd46fac5c2d6c1636dcfc14358a20c70e22daf89e1a759b629 + languageName: node + linkType: hard + +"@dabh/diagnostics@npm:^2.0.2": + version: 2.0.3 + resolution: "@dabh/diagnostics@npm:2.0.3" + dependencies: + colorspace: "npm:1.1.x" + enabled: "npm:2.0.x" + kuler: "npm:^2.0.0" + checksum: 10c0/a5133df8492802465ed01f2f0a5784585241a1030c362d54a602ed1839816d6c93d71dde05cf2ddb4fd0796238c19774406bd62fa2564b637907b495f52425fe + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.2.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: "npm:^3.3.0" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/7e559c4ce59cd3a06b1b5a517b593912e680a7f981ae7affab0d01d709e99cd5647019be8fafa38c350305bc32f1f7d42c7073edde2ab536c745e365f37b607e + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.4.0, @eslint-community/regexpp@npm:^4.6.1": + version: 4.10.1 + resolution: "@eslint-community/regexpp@npm:4.10.1" + checksum: 10c0/f59376025d0c91dd9fdf18d33941df499292a3ecba3e9889c360f3f6590197d30755604588786cdca0f9030be315a26b206014af4b65c0ff85b4ec49043de780 + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" + dependencies: + ajv: "npm:^6.12.4" + debug: "npm:^4.3.2" + espree: "npm:^9.6.0" + globals: "npm:^13.19.0" + ignore: "npm:^5.2.0" + import-fresh: "npm:^3.2.1" + js-yaml: "npm:^4.1.0" + minimatch: "npm:^3.1.2" + strip-json-comments: "npm:^3.1.1" + checksum: 10c0/32f67052b81768ae876c84569ffd562491ec5a5091b0c1e1ca1e0f3c24fb42f804952fdd0a137873bc64303ba368a71ba079a6f691cee25beee9722d94cc8573 + languageName: node + linkType: hard + +"@eslint/js@npm:8.57.0": + version: 8.57.0 + resolution: "@eslint/js@npm:8.57.0" + checksum: 10c0/9a518bb8625ba3350613903a6d8c622352ab0c6557a59fe6ff6178bf882bf57123f9d92aa826ee8ac3ee74b9c6203fe630e9ee00efb03d753962dcf65ee4bd94 + languageName: node + linkType: hard + +"@humanwhocodes/config-array@npm:^0.11.14": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" + dependencies: + "@humanwhocodes/object-schema": "npm:^2.0.2" + debug: "npm:^4.3.1" + minimatch: "npm:^3.0.5" + checksum: 10c0/66f725b4ee5fdd8322c737cb5013e19fac72d4d69c8bf4b7feb192fcb83442b035b92186f8e9497c220e58b2d51a080f28a73f7899bc1ab288c3be172c467541 + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 10c0/909b69c3b86d482c26b3359db16e46a32e0fb30bd306a3c176b8313b9e7313dba0f37f519de6aa8b0a1921349e505f259d19475e123182416a506d7f87e7f529 + languageName: node + linkType: hard + +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.3 + resolution: "@humanwhocodes/object-schema@npm:2.0.3" + checksum: 10c0/80520eabbfc2d32fe195a93557cef50dfe8c8905de447f022675aaf66abc33ae54098f5ea78548d925aa671cd4ab7c7daa5ad704fe42358c9b5e7db60f80696c + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@istanbuljs/load-nyc-config@npm:^1.0.0": + version: 1.1.0 + resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" + dependencies: + camelcase: "npm:^5.3.1" + find-up: "npm:^4.1.0" + get-package-type: "npm:^0.1.0" + js-yaml: "npm:^3.13.1" + resolve-from: "npm:^5.0.0" + checksum: 10c0/dd2a8b094887da5a1a2339543a4933d06db2e63cbbc2e288eb6431bd832065df0c099d091b6a67436e71b7d6bf85f01ce7c15f9253b4cbebcc3b9a496165ba42 + languageName: node + linkType: hard + +"@istanbuljs/schema@npm:^0.1.2, @istanbuljs/schema@npm:^0.1.3": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 10c0/61c5286771676c9ca3eb2bd8a7310a9c063fb6e0e9712225c8471c582d157392c88f5353581c8c9adbe0dff98892317d2fdfc56c3499aa42e0194405206a963a + languageName: node + linkType: hard + +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10c0/7be408781d0a6f657e969cbec13b540c329671819c2f57acfad0dae9dbfe2c9be859f38fe99b35dba9ff1536937dc6ddc69fdcd2794812fa3c647a1619797f6c + languageName: node + linkType: hard + +"@jest/core@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/core@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/reporters": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-changed-files: "npm:^29.7.0" + jest-config: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-resolve-dependencies: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-ansi: "npm:^6.0.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/934f7bf73190f029ac0f96662c85cd276ec460d407baf6b0dbaec2872e157db4d55a7ee0b1c43b18874602f662b37cb973dda469a4e6d88b4e4845b521adeeb2 + languageName: node + linkType: hard + +"@jest/environment@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/environment@npm:29.7.0" + dependencies: + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + checksum: 10c0/c7b1b40c618f8baf4d00609022d2afa086d9c6acc706f303a70bb4b67275868f620ad2e1a9efc5edd418906157337cce50589a627a6400bbdf117d351b91ef86 + languageName: node + linkType: hard + +"@jest/expect-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect-utils@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + checksum: 10c0/60b79d23a5358dc50d9510d726443316253ecda3a7fb8072e1526b3e0d3b14f066ee112db95699b7a43ad3f0b61b750c72e28a5a1cac361d7a2bb34747fa938a + languageName: node + linkType: hard + +"@jest/expect@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect@npm:29.7.0" + dependencies: + expect: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + checksum: 10c0/b41f193fb697d3ced134349250aed6ccea075e48c4f803159db102b826a4e473397c68c31118259868fd69a5cba70e97e1c26d2c2ff716ca39dc73a2ccec037e + languageName: node + linkType: hard + +"@jest/fake-timers@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/fake-timers@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@sinonjs/fake-timers": "npm:^10.0.2" + "@types/node": "npm:*" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10c0/cf0a8bcda801b28dc2e2b2ba36302200ee8104a45ad7a21e6c234148932f826cb3bc57c8df3b7b815aeea0861d7b6ca6f0d4778f93b9219398ef28749e03595c + languageName: node + linkType: hard + +"@jest/globals@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/globals@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + jest-mock: "npm:^29.7.0" + checksum: 10c0/a385c99396878fe6e4460c43bd7bb0a5cc52befb462cc6e7f2a3810f9e7bcce7cdeb51908fd530391ee452dc856c98baa2c5f5fa8a5b30b071d31ef7f6955cea + languageName: node + linkType: hard + +"@jest/reporters@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/reporters@npm:29.7.0" + dependencies: + "@bcoe/v8-coverage": "npm:^0.2.3" + "@jest/console": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + collect-v8-coverage: "npm:^1.0.0" + exit: "npm:^0.1.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + istanbul-lib-coverage: "npm:^3.0.0" + istanbul-lib-instrument: "npm:^6.0.0" + istanbul-lib-report: "npm:^3.0.0" + istanbul-lib-source-maps: "npm:^4.0.0" + istanbul-reports: "npm:^3.1.3" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + slash: "npm:^3.0.0" + string-length: "npm:^4.0.1" + strip-ansi: "npm:^6.0.0" + v8-to-istanbul: "npm:^9.0.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/a754402a799541c6e5aff2c8160562525e2a47e7d568f01ebfc4da66522de39cbb809bbb0a841c7052e4270d79214e70aec3c169e4eae42a03bc1a8a20cb9fa2 + languageName: node + linkType: hard + +"@jest/schemas@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/schemas@npm:29.6.3" + dependencies: + "@sinclair/typebox": "npm:^0.27.8" + checksum: 10c0/b329e89cd5f20b9278ae1233df74016ebf7b385e0d14b9f4c1ad18d096c4c19d1e687aa113a9c976b16ec07f021ae53dea811fb8c1248a50ac34fbe009fdf6be + languageName: node + linkType: hard + +"@jest/source-map@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/source-map@npm:29.6.3" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.18" + callsites: "npm:^3.0.0" + graceful-fs: "npm:^4.2.9" + checksum: 10c0/a2f177081830a2e8ad3f2e29e20b63bd40bade294880b595acf2fc09ec74b6a9dd98f126a2baa2bf4941acd89b13a4ade5351b3885c224107083a0059b60a219 + languageName: node + linkType: hard + +"@jest/test-result@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-result@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + collect-v8-coverage: "npm:^1.0.0" + checksum: 10c0/7de54090e54a674ca173470b55dc1afdee994f2d70d185c80236003efd3fa2b753fff51ffcdda8e2890244c411fd2267529d42c4a50a8303755041ee493e6a04 + languageName: node + linkType: hard + +"@jest/test-sequencer@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-sequencer@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10c0/593a8c4272797bb5628984486080cbf57aed09c7cfdc0a634e8c06c38c6bef329c46c0016e84555ee55d1cd1f381518cf1890990ff845524c1123720c8c1481b + languageName: node + linkType: hard + +"@jest/transform@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/transform@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + babel-plugin-istanbul: "npm:^6.1.1" + chalk: "npm:^4.0.0" + convert-source-map: "npm:^2.0.0" + fast-json-stable-stringify: "npm:^2.1.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pirates: "npm:^4.0.4" + slash: "npm:^3.0.0" + write-file-atomic: "npm:^4.0.2" + checksum: 10c0/7f4a7f73dcf45dfdf280c7aa283cbac7b6e5a904813c3a93ead7e55873761fc20d5c4f0191d2019004fac6f55f061c82eb3249c2901164ad80e362e7a7ede5a6 + languageName: node + linkType: hard + +"@jest/types@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/types@npm:29.6.3" + dependencies: + "@jest/schemas": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + "@types/istanbul-reports": "npm:^3.0.0" + "@types/node": "npm:*" + "@types/yargs": "npm:^17.0.8" + chalk: "npm:^4.0.0" + checksum: 10c0/ea4e493dd3fb47933b8ccab201ae573dcc451f951dc44ed2a86123cd8541b82aa9d2b1031caf9b1080d6673c517e2dcc25a44b2dc4f3fbc37bfc965d444888c0 + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" + dependencies: + "@jridgewell/set-array": "npm:^1.2.1" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/1be4fd4a6b0f41337c4f5fdf4afc3bd19e39c3691924817108b82ffcb9c9e609c273f936932b9fba4b3a298ce2eb06d9bff4eb1cc3bd81c4f4ee1b4917e25feb + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2" + checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e + languageName: node + linkType: hard + +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 10c0/2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": + version: 1.4.15 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" + checksum: 10c0/0c6b5ae663087558039052a626d2d7ed5208da36cfd707dcc5cea4a07cfc918248403dcb5989a8f7afaf245ce0573b7cc6fd94c4a30453bd10e44d9363940ba5 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + languageName: node + linkType: hard + +"@mapbox/node-pre-gyp@npm:^1.0.11": + version: 1.0.11 + resolution: "@mapbox/node-pre-gyp@npm:1.0.11" + dependencies: + detect-libc: "npm:^2.0.0" + https-proxy-agent: "npm:^5.0.0" + make-dir: "npm:^3.1.0" + node-fetch: "npm:^2.6.7" + nopt: "npm:^5.0.0" + npmlog: "npm:^5.0.1" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.11" + bin: + node-pre-gyp: bin/node-pre-gyp + checksum: 10c0/2b24b93c31beca1c91336fa3b3769fda98e202fb7f9771f0f4062588d36dcc30fcf8118c36aa747fa7f7610d8cf601872bdaaf62ce7822bb08b545d1bbe086cc + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 10c0/732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: 10c0/db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"@sinclair/typebox@npm:^0.27.8": + version: 0.27.8 + resolution: "@sinclair/typebox@npm:0.27.8" + checksum: 10c0/ef6351ae073c45c2ac89494dbb3e1f87cc60a93ce4cde797b782812b6f97da0d620ae81973f104b43c9b7eaa789ad20ba4f6a1359f1cc62f63729a55a7d22d4e + languageName: node + linkType: hard + +"@sinonjs/commons@npm:^3.0.0": + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" + dependencies: + type-detect: "npm:4.0.8" + checksum: 10c0/1227a7b5bd6c6f9584274db996d7f8cee2c8c350534b9d0141fc662eaf1f292ea0ae3ed19e5e5271c8fd390d27e492ca2803acd31a1978be2cdc6be0da711403 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^10.0.2": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": "npm:^3.0.0" + checksum: 10c0/2e2fb6cc57f227912814085b7b01fede050cd4746ea8d49a1e44d5a0e56a804663b0340ae2f11af7559ea9bf4d087a11f2f646197a660ea3cb04e19efc04aa63 + languageName: node + linkType: hard + +"@types/babel__core@npm:^7.1.14": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" + dependencies: + "@babel/parser": "npm:^7.20.7" + "@babel/types": "npm:^7.20.7" + "@types/babel__generator": "npm:*" + "@types/babel__template": "npm:*" + "@types/babel__traverse": "npm:*" + checksum: 10c0/bdee3bb69951e833a4b811b8ee9356b69a61ed5b7a23e1a081ec9249769117fa83aaaf023bb06562a038eb5845155ff663e2d5c75dd95c1d5ccc91db012868ff + languageName: node + linkType: hard + +"@types/babel__generator@npm:*": + version: 7.6.8 + resolution: "@types/babel__generator@npm:7.6.8" + dependencies: + "@babel/types": "npm:^7.0.0" + checksum: 10c0/f0ba105e7d2296bf367d6e055bb22996886c114261e2cb70bf9359556d0076c7a57239d019dee42bb063f565bade5ccb46009bce2044b2952d964bf9a454d6d2 + languageName: node + linkType: hard + +"@types/babel__template@npm:*": + version: 7.4.4 + resolution: "@types/babel__template@npm:7.4.4" + dependencies: + "@babel/parser": "npm:^7.1.0" + "@babel/types": "npm:^7.0.0" + checksum: 10c0/cc84f6c6ab1eab1427e90dd2b76ccee65ce940b778a9a67be2c8c39e1994e6f5bbc8efa309f6cea8dc6754994524cd4d2896558df76d92e7a1f46ecffee7112b + languageName: node + linkType: hard + +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": + version: 7.20.6 + resolution: "@types/babel__traverse@npm:7.20.6" + dependencies: + "@babel/types": "npm:^7.20.7" + checksum: 10c0/7ba7db61a53e28cac955aa99af280d2600f15a8c056619c05b6fc911cbe02c61aa4f2823299221b23ce0cce00b294c0e5f618ec772aa3f247523c2e48cf7b888 + languageName: node + linkType: hard + +"@types/bcrypt@npm:^5.0.2": + version: 5.0.2 + resolution: "@types/bcrypt@npm:5.0.2" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/dd7f05e183b9b1fc08ec499069febf197ab8e9c720766b5bbb5628395082e248f9a444c60882fe7788361fcadc302e21e055ab9c26a300f100e08791c353e6aa + languageName: node + linkType: hard + +"@types/better-sqlite3@npm:^7.6.12": + version: 7.6.12 + resolution: "@types/better-sqlite3@npm:7.6.12" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/5367de7492e2c697aa20cc4024ba26210971d15f60c01ef691eddfbbfd39ccf9f80d5129fd7fd6c76c98804739325e23d2b156b0eac8f5a7665ba374a08ac1e7 + languageName: node + linkType: hard + +"@types/body-parser@npm:*": + version: 1.19.5 + resolution: "@types/body-parser@npm:1.19.5" + dependencies: + "@types/connect": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/aebeb200f25e8818d8cf39cd0209026750d77c9b85381cdd8deeb50913e4d18a1ebe4b74ca9b0b4d21952511eeaba5e9fbbf739b52731a2061e206ec60d568df + languageName: node + linkType: hard + +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c + languageName: node + linkType: hard + +"@types/cookiejar@npm:^2.1.5": + version: 2.1.5 + resolution: "@types/cookiejar@npm:2.1.5" + checksum: 10c0/af38c3d84aebb3ccc6e46fb6afeeaac80fb26e63a487dd4db5a8b87e6ad3d4b845ba1116b2ae90d6f886290a36200fa433d8b1f6fe19c47da6b81872ce9a2764 + languageName: node + linkType: hard + +"@types/cors@npm:^2.8.13": + version: 2.8.17 + resolution: "@types/cors@npm:2.8.17" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/457364c28c89f3d9ed34800e1de5c6eaaf344d1bb39af122f013322a50bc606eb2aa6f63de4e41a7a08ba7ef454473926c94a830636723da45bf786df032696d + languageName: node + linkType: hard + +"@types/express-actuator@npm:^1.8.0": + version: 1.8.3 + resolution: "@types/express-actuator@npm:1.8.3" + dependencies: + "@types/express": "npm:*" + checksum: 10c0/e7a5ffae28aa89c636edc75594adbf63bc3a0f86d27d0bdd4567e8ac91d6de8d0b76b85009ab65a9714625cfc443374eab227b11e58cb4450093500123eef0a2 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^4.17.33": + version: 4.19.3 + resolution: "@types/express-serve-static-core@npm:4.19.3" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10c0/5d2a1fb96a17a8e0e8c59325dfeb6d454bbc5c9b9b6796eec0397ddf9dbd262892040d5da3d72b5d7148f34bb3fcd438faf1b37fcba8c5a03e75fae491ad1edf + languageName: node + linkType: hard + +"@types/express@npm:*, @types/express@npm:^4.17.17": + version: 4.17.21 + resolution: "@types/express@npm:4.17.21" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^4.17.33" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 10c0/12e562c4571da50c7d239e117e688dc434db1bac8be55613294762f84fd77fbd0658ccd553c7d3ab02408f385bc93980992369dd30e2ecd2c68c358e6af8fabf + languageName: node + linkType: hard + +"@types/graceful-fs@npm:^4.1.3": + version: 4.1.9 + resolution: "@types/graceful-fs@npm:4.1.9" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/235d2fc69741448e853333b7c3d1180a966dd2b8972c8cbcd6b2a0c6cd7f8d582ab2b8e58219dbc62cce8f1b40aa317ff78ea2201cdd8249da5025adebed6f0b + languageName: node + linkType: hard + +"@types/http-errors@npm:*": + version: 2.0.4 + resolution: "@types/http-errors@npm:2.0.4" + checksum: 10c0/494670a57ad4062fee6c575047ad5782506dd35a6b9ed3894cea65830a94367bd84ba302eb3dde331871f6d70ca287bfedb1b2cf658e6132cd2cbd427ab56836 + languageName: node + linkType: hard + +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": + version: 2.0.6 + resolution: "@types/istanbul-lib-coverage@npm:2.0.6" + checksum: 10c0/3948088654f3eeb45363f1db158354fb013b362dba2a5c2c18c559484d5eb9f6fd85b23d66c0a7c2fcfab7308d0a585b14dadaca6cc8bf89ebfdc7f8f5102fb7 + languageName: node + linkType: hard + +"@types/istanbul-lib-report@npm:*": + version: 3.0.3 + resolution: "@types/istanbul-lib-report@npm:3.0.3" + dependencies: + "@types/istanbul-lib-coverage": "npm:*" + checksum: 10c0/247e477bbc1a77248f3c6de5dadaae85ff86ac2d76c5fc6ab1776f54512a745ff2a5f791d22b942e3990ddbd40f3ef5289317c4fca5741bedfaa4f01df89051c + languageName: node + linkType: hard + +"@types/istanbul-reports@npm:^3.0.0": + version: 3.0.4 + resolution: "@types/istanbul-reports@npm:3.0.4" + dependencies: + "@types/istanbul-lib-report": "npm:*" + checksum: 10c0/1647fd402aced5b6edac87274af14ebd6b3a85447ef9ad11853a70fd92a98d35f81a5d3ea9fcb5dbb5834e800c6e35b64475e33fcae6bfa9acc70d61497c54ee + languageName: node + linkType: hard + +"@types/jest@npm:^29.2.3": + version: 29.5.12 + resolution: "@types/jest@npm:29.5.12" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 10c0/25fc8e4c611fa6c4421e631432e9f0a6865a8cb07c9815ec9ac90d630271cad773b2ee5fe08066f7b95bebd18bb967f8ce05d018ee9ab0430f9dfd1d84665b6f + languageName: node + linkType: hard + +"@types/json-schema@npm:^7.0.9": + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db + languageName: node + linkType: hard + +"@types/methods@npm:^1.1.4": + version: 1.1.4 + resolution: "@types/methods@npm:1.1.4" + checksum: 10c0/a78534d79c300718298bfff92facd07bf38429c66191f640c1db4c9cff1e36f819304298a96f7536b6512bfc398e5c3e6b831405e138cd774b88ad7be78d682a + languageName: node + linkType: hard + +"@types/mime@npm:^1": + version: 1.3.5 + resolution: "@types/mime@npm:1.3.5" + checksum: 10c0/c2ee31cd9b993804df33a694d5aa3fa536511a49f2e06eeab0b484fef59b4483777dbb9e42a4198a0809ffbf698081fdbca1e5c2218b82b91603dfab10a10fbc + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 20.14.5 + resolution: "@types/node@npm:20.14.5" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/06a8c304b5f7f190d4497807dc67ad09ee7b14ea2996bfdc823553c624698d8cab1ef9d16f8b764f20cb9eb11caa0e832787741e9ef70e1c89d620797ab28436 + languageName: node + linkType: hard + +"@types/node@npm:^17.0.45": + version: 17.0.45 + resolution: "@types/node@npm:17.0.45" + checksum: 10c0/0db377133d709b33a47892581a21a41cd7958f22723a3cc6c71d55ac018121382de42fbfc7970d5ae3e7819dbe5f40e1c6a5174aedf7e7964e9cb8fa72b580b0 + languageName: node + linkType: hard + +"@types/qs@npm:*": + version: 6.9.15 + resolution: "@types/qs@npm:6.9.15" + checksum: 10c0/49c5ff75ca3adb18a1939310042d273c9fc55920861bd8e5100c8a923b3cda90d759e1a95e18334092da1c8f7b820084687770c83a1ccef04fb2c6908117c823 + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 10c0/361bb3e964ec5133fa40644a0b942279ed5df1949f21321d77de79f48b728d39253e5ce0408c9c17e4e0fd95ca7899da36841686393b9f7a1e209916e9381a3c + languageName: node + linkType: hard + +"@types/semver@npm:^7.3.12": + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: 10c0/8663ff927234d1c5fcc04b33062cb2b9fcfbe0f5f351ed26c4d1e1581657deebd506b41ff7fdf89e787e3d33ce05854bc01686379b89e9c49b564c4cfa988efa + languageName: node + linkType: hard + +"@types/send@npm:*": + version: 0.17.4 + resolution: "@types/send@npm:0.17.4" + dependencies: + "@types/mime": "npm:^1" + "@types/node": "npm:*" + checksum: 10c0/7f17fa696cb83be0a104b04b424fdedc7eaba1c9a34b06027239aba513b398a0e2b7279778af521f516a397ced417c96960e5f50fcfce40c4bc4509fb1a5883c + languageName: node + linkType: hard + +"@types/serve-static@npm:*": + version: 1.15.7 + resolution: "@types/serve-static@npm:1.15.7" + dependencies: + "@types/http-errors": "npm:*" + "@types/node": "npm:*" + "@types/send": "npm:*" + checksum: 10c0/26ec864d3a626ea627f8b09c122b623499d2221bbf2f470127f4c9ebfe92bd8a6bb5157001372d4c4bd0dd37a1691620217d9dc4df5aa8f779f3fd996b1c60ae + languageName: node + linkType: hard + +"@types/stack-utils@npm:^2.0.0": + version: 2.0.3 + resolution: "@types/stack-utils@npm:2.0.3" + checksum: 10c0/1f4658385ae936330581bcb8aa3a066df03867d90281cdf89cc356d404bd6579be0f11902304e1f775d92df22c6dd761d4451c804b0a4fba973e06211e9bd77c + languageName: node + linkType: hard + +"@types/superagent@npm:*": + version: 8.1.7 + resolution: "@types/superagent@npm:8.1.7" + dependencies: + "@types/cookiejar": "npm:^2.1.5" + "@types/methods": "npm:^1.1.4" + "@types/node": "npm:*" + checksum: 10c0/4676d539f5feaaea9d39d7409c86ae9e15b92a43c28456aff9d9897e47e9fe5ebd3807600c5310f84fe5ebea30f3fe5e2b3b101a87821a478ca79e3a56fd8c9e + languageName: node + linkType: hard + +"@types/supertest@npm:^2.0.12": + version: 2.0.16 + resolution: "@types/supertest@npm:2.0.16" + dependencies: + "@types/superagent": "npm:*" + checksum: 10c0/e1b4a4d788c19cd92a3f2e6d0979fb0f679c49aefae2011895a4d9c35aa960d43463aca8783a0b3382bbf0b4eb7ceaf8752d7dc80b8f5a9644fa14e1b1bdbc90 + languageName: node + linkType: hard + +"@types/triple-beam@npm:^1.3.2": + version: 1.3.5 + resolution: "@types/triple-beam@npm:1.3.5" + checksum: 10c0/d5d7f25da612f6d79266f4f1bb9c1ef8f1684e9f60abab251e1261170631062b656ba26ff22631f2760caeafd372abc41e64867cde27fba54fafb73a35b9056a + languageName: node + linkType: hard + +"@types/uuid@npm:^9.0.0": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: 10c0/b411b93054cb1d4361919579ef3508a1f12bf15b5fdd97337d3d351bece6c921b52b6daeef89b62340fd73fd60da407878432a1af777f40648cbe53a01723489 + languageName: node + linkType: hard + +"@types/yargs-parser@npm:*": + version: 21.0.3 + resolution: "@types/yargs-parser@npm:21.0.3" + checksum: 10c0/e71c3bd9d0b73ca82e10bee2064c384ab70f61034bbfb78e74f5206283fc16a6d85267b606b5c22cb2a3338373586786fed595b2009825d6a9115afba36560a0 + languageName: node + linkType: hard + +"@types/yargs@npm:^17.0.8": + version: 17.0.32 + resolution: "@types/yargs@npm:17.0.32" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: 10c0/2095e8aad8a4e66b86147415364266b8d607a3b95b4239623423efd7e29df93ba81bb862784a6e08664f645cc1981b25fd598f532019174cd3e5e1e689e1cccf + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:^5.51.0": + version: 5.62.0 + resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:5.62.0" + "@typescript-eslint/type-utils": "npm:5.62.0" + "@typescript-eslint/utils": "npm:5.62.0" + debug: "npm:^4.3.4" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.0" + natural-compare-lite: "npm:^1.4.0" + semver: "npm:^7.3.7" + tsutils: "npm:^3.21.0" + peerDependencies: + "@typescript-eslint/parser": ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/3f40cb6bab5a2833c3544e4621b9fdacd8ea53420cadc1c63fac3b89cdf5c62be1e6b7bcf56976dede5db4c43830de298ced3db60b5494a3b961ca1b4bff9f2a + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:^5.51.0": + version: 5.62.0 + resolution: "@typescript-eslint/parser@npm:5.62.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:5.62.0" + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/typescript-estree": "npm:5.62.0" + debug: "npm:^4.3.4" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/315194b3bf39beb9bd16c190956c46beec64b8371e18d6bb72002108b250983eb1e186a01d34b77eb4045f4941acbb243b16155fbb46881105f65e37dc9e24d4 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/scope-manager@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/visitor-keys": "npm:5.62.0" + checksum: 10c0/861253235576c1c5c1772d23cdce1418c2da2618a479a7de4f6114a12a7ca853011a1e530525d0931c355a8fd237b9cd828fac560f85f9623e24054fd024726f + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/type-utils@npm:5.62.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:5.62.0" + "@typescript-eslint/utils": "npm:5.62.0" + debug: "npm:^4.3.4" + tsutils: "npm:^3.21.0" + peerDependencies: + eslint: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/93112e34026069a48f0484b98caca1c89d9707842afe14e08e7390af51cdde87378df29d213d3bbd10a7cfe6f91b228031b56218515ce077bdb62ddea9d9f474 + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/types@npm:5.62.0" + checksum: 10c0/7febd3a7f0701c0b927e094f02e82d8ee2cada2b186fcb938bc2b94ff6fbad88237afc304cbaf33e82797078bbbb1baf91475f6400912f8b64c89be79bfa4ddf + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/visitor-keys": "npm:5.62.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + semver: "npm:^7.3.7" + tsutils: "npm:^3.21.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/d7984a3e9d56897b2481940ec803cb8e7ead03df8d9cfd9797350be82ff765dfcf3cfec04e7355e1779e948da8f02bc5e11719d07a596eb1cb995c48a95e38cf + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/utils@npm:5.62.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@types/json-schema": "npm:^7.0.9" + "@types/semver": "npm:^7.3.12" + "@typescript-eslint/scope-manager": "npm:5.62.0" + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/typescript-estree": "npm:5.62.0" + eslint-scope: "npm:^5.1.1" + semver: "npm:^7.3.7" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/f09b7d9952e4a205eb1ced31d7684dd55cee40bf8c2d78e923aa8a255318d97279825733902742c09d8690f37a50243f4c4d383ab16bd7aefaf9c4b438f785e1 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + eslint-visitor-keys: "npm:^3.3.0" + checksum: 10c0/7c3b8e4148e9b94d9b7162a596a1260d7a3efc4e65199693b8025c71c4652b8042501c0bc9f57654c1e2943c26da98c0f77884a746c6ae81389fcb0b513d995d + languageName: node + linkType: hard + +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 10c0/8209c937cb39119f44eb63cf90c0b73e7c754209a6411c707be08e50e29ee81356dca1a848a405c8bdeebfe2f5e4f831ad310ae1689eeef65e7445c090c6657d + languageName: node + linkType: hard + +"abbrev@npm:1": + version: 1.1.1 + resolution: "abbrev@npm:1.1.1" + checksum: 10c0/3f762677702acb24f65e813070e306c61fafe25d4b2583f9dfc935131f774863f3addd5741572ed576bd69cabe473c5af18e1e108b829cb7b6b4747884f726e6 + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: 10c0/f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + languageName: node + linkType: hard + +"accepts@npm:~1.3.8": + version: 1.3.8 + resolution: "accepts@npm:1.3.8" + dependencies: + mime-types: "npm:~2.1.34" + negotiator: "npm:0.6.3" + checksum: 10c0/3a35c5f5586cfb9a21163ca47a5f77ac34fa8ceb5d17d2fa2c0d81f41cbd7f8c6fa52c77e2c039acc0f4d09e71abdc51144246900f6bef5e3c4b333f77d89362 + languageName: node + linkType: hard + +"acorn-jsx@npm:^5.3.2": + version: 5.3.2 + resolution: "acorn-jsx@npm:5.3.2" + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/4c54868fbef3b8d58927d5e33f0a4de35f59012fe7b12cf9dfbb345fb8f46607709e1c4431be869a23fb63c151033d84c4198fa9f79385cec34fcb1dd53974c1 + languageName: node + linkType: hard + +"acorn@npm:^8.9.0": + version: 8.12.0 + resolution: "acorn@npm:8.12.0" + bin: + acorn: bin/acorn + checksum: 10c0/a19f9dead009d3b430fa3c253710b47778cdaace15b316de6de93a68c355507bc1072a9956372b6c990cbeeb167d4a929249d0faeb8ae4bb6911d68d53299549 + languageName: node + linkType: hard + +"actual-sync@workspace:.": + version: 0.0.0-use.local + resolution: "actual-sync@workspace:." + dependencies: + "@actual-app/crdt": "npm:2.1.0" + "@actual-app/web": "npm:24.12.0" + "@babel/preset-typescript": "npm:^7.20.2" + "@types/bcrypt": "npm:^5.0.2" + "@types/better-sqlite3": "npm:^7.6.12" + "@types/cors": "npm:^2.8.13" + "@types/express": "npm:^4.17.17" + "@types/express-actuator": "npm:^1.8.0" + "@types/jest": "npm:^29.2.3" + "@types/node": "npm:^17.0.45" + "@types/supertest": "npm:^2.0.12" + "@types/uuid": "npm:^9.0.0" + "@typescript-eslint/eslint-plugin": "npm:^5.51.0" + "@typescript-eslint/parser": "npm:^5.51.0" + bcrypt: "npm:^5.1.1" + better-sqlite3: "npm:^11.7.0" + body-parser: "npm:^1.20.3" + cors: "npm:^2.8.5" + date-fns: "npm:^2.30.0" + debug: "npm:^4.3.4" + eslint: "npm:^8.33.0" + eslint-plugin-prettier: "npm:^4.2.1" + express: "npm:4.20.0" + express-actuator: "npm:1.8.4" + express-rate-limit: "npm:^6.7.0" + express-response-size: "npm:^0.0.3" + express-winston: "npm:^4.2.0" + jest: "npm:^29.3.1" + jws: "npm:^4.0.0" + migrate: "npm:^2.0.1" + nordigen-node: "npm:^1.4.0" + openid-client: "npm:^5.4.2" + prettier: "npm:^2.8.3" + supertest: "npm:^6.3.1" + typescript: "npm:^4.9.5" + uuid: "npm:^9.0.0" + winston: "npm:^3.14.2" + languageName: unknown + linkType: soft + +"agent-base@npm:6": + version: 6.0.2 + resolution: "agent-base@npm:6.0.2" + dependencies: + debug: "npm:4" + checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261 + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" + dependencies: + debug: "npm:^4.3.4" + checksum: 10c0/e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + languageName: node + linkType: hard + +"ajv@npm:^6.12.4": + version: 6.12.6 + resolution: "ajv@npm:6.12.6" + dependencies: + fast-deep-equal: "npm:^3.1.1" + fast-json-stable-stringify: "npm:^2.0.0" + json-schema-traverse: "npm:^0.4.1" + uri-js: "npm:^4.2.2" + checksum: 10c0/41e23642cbe545889245b9d2a45854ebba51cda6c778ebced9649420d9205f2efb39cb43dbc41e358409223b1ea43303ae4839db682c848b891e4811da1a5a71 + languageName: node + linkType: hard + +"ansi-escapes@npm:^4.2.1": + version: 4.3.2 + resolution: "ansi-escapes@npm:4.3.2" + dependencies: + type-fest: "npm:^0.21.3" + checksum: 10c0/da917be01871525a3dfcf925ae2977bc59e8c513d4423368645634bf5d4ceba5401574eb705c1e92b79f7292af5a656f78c5725a4b0e1cec97c4b413705c1d50 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 10c0/cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 + languageName: node + linkType: hard + +"ansi-styles@npm:^3.2.1": + version: 3.2.1 + resolution: "ansi-styles@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.0" + checksum: 10c0/ece5a8ef069fcc5298f67e3f4771a663129abd174ea2dfa87923a2be2abf6cd367ef72ac87942da00ce85bd1d651d4cd8595aebdb1b385889b89b205860e977b + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^5.0.0": + version: 5.2.0 + resolution: "ansi-styles@npm:5.2.0" + checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"anymatch@npm:^3.0.3": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + +"aproba@npm:^1.0.3 || ^2.0.0": + version: 2.0.0 + resolution: "aproba@npm:2.0.0" + checksum: 10c0/d06e26384a8f6245d8c8896e138c0388824e259a329e0c9f196b4fa533c82502a6fd449586e3604950a0c42921832a458bb3aa0aa9f0ba449cfd4f50fd0d09b5 + languageName: node + linkType: hard + +"are-we-there-yet@npm:^2.0.0": + version: 2.0.0 + resolution: "are-we-there-yet@npm:2.0.0" + dependencies: + delegates: "npm:^1.0.0" + readable-stream: "npm:^3.6.0" + checksum: 10c0/375f753c10329153c8d66dc95e8f8b6c7cc2aa66e05cb0960bd69092b10dae22900cacc7d653ad11d26b3ecbdbfe1e8bfb6ccf0265ba8077a7d979970f16b99c + languageName: node + linkType: hard + +"argparse@npm:^1.0.7": + version: 1.0.10 + resolution: "argparse@npm:1.0.10" + dependencies: + sprintf-js: "npm:~1.0.2" + checksum: 10c0/b2972c5c23c63df66bca144dbc65d180efa74f25f8fd9b7d9a0a6c88ae839db32df3d54770dcb6460cf840d232b60695d1a6b1053f599d84e73f7437087712de + languageName: node + linkType: hard + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e + languageName: node + linkType: hard + +"array-flatten@npm:1.1.1": + version: 1.1.1 + resolution: "array-flatten@npm:1.1.1" + checksum: 10c0/806966c8abb2f858b08f5324d9d18d7737480610f3bd5d3498aaae6eb5efdc501a884ba019c9b4a8f02ff67002058749d05548fd42fa8643f02c9c7f22198b91 + languageName: node + linkType: hard + +"array-union@npm:^2.1.0": + version: 2.1.0 + resolution: "array-union@npm:2.1.0" + checksum: 10c0/429897e68110374f39b771ec47a7161fc6a8fc33e196857c0a396dc75df0b5f65e4d046674db764330b6bb66b39ef48dd7c53b6a2ee75cfb0681e0c1a7033962 + languageName: node + linkType: hard + +"asap@npm:^2.0.0": + version: 2.0.6 + resolution: "asap@npm:2.0.6" + checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d + languageName: node + linkType: hard + +"async@npm:^3.2.3": + version: 3.2.5 + resolution: "async@npm:3.2.5" + checksum: 10c0/1408287b26c6db67d45cb346e34892cee555b8b59e6c68e6f8c3e495cad5ca13b4f218180e871f3c2ca30df4ab52693b66f2f6ff43644760cab0b2198bda79c1 + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d + languageName: node + linkType: hard + +"axios@npm:^1.2.1": + version: 1.7.4 + resolution: "axios@npm:1.7.4" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10c0/5ea1a93140ca1d49db25ef8e1bd8cfc59da6f9220159a944168860ad15a2743ea21c5df2967795acb15cbe81362f5b157fdebbea39d53117ca27658bab9f7f17 + languageName: node + linkType: hard + +"babel-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "babel-jest@npm:29.7.0" + dependencies: + "@jest/transform": "npm:^29.7.0" + "@types/babel__core": "npm:^7.1.14" + babel-plugin-istanbul: "npm:^6.1.1" + babel-preset-jest: "npm:^29.6.3" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + slash: "npm:^3.0.0" + peerDependencies: + "@babel/core": ^7.8.0 + checksum: 10c0/2eda9c1391e51936ca573dd1aedfee07b14c59b33dbe16ef347873ddd777bcf6e2fc739681e9e9661ab54ef84a3109a03725be2ac32cd2124c07ea4401cbe8c1 + languageName: node + linkType: hard + +"babel-plugin-istanbul@npm:^6.1.1": + version: 6.1.1 + resolution: "babel-plugin-istanbul@npm:6.1.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.0.0" + "@istanbuljs/load-nyc-config": "npm:^1.0.0" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-instrument: "npm:^5.0.4" + test-exclude: "npm:^6.0.0" + checksum: 10c0/1075657feb705e00fd9463b329921856d3775d9867c5054b449317d39153f8fbcebd3e02ebf00432824e647faff3683a9ca0a941325ef1afe9b3c4dd51b24beb + languageName: node + linkType: hard + +"babel-plugin-jest-hoist@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-plugin-jest-hoist@npm:29.6.3" + dependencies: + "@babel/template": "npm:^7.3.3" + "@babel/types": "npm:^7.3.3" + "@types/babel__core": "npm:^7.1.14" + "@types/babel__traverse": "npm:^7.0.6" + checksum: 10c0/7e6451caaf7dce33d010b8aafb970e62f1b0c0b57f4978c37b0d457bbcf0874d75a395a102daf0bae0bd14eafb9f6e9a165ee5e899c0a4f1f3bb2e07b304ed2e + languageName: node + linkType: hard + +"babel-preset-current-node-syntax@npm:^1.0.0": + version: 1.0.1 + resolution: "babel-preset-current-node-syntax@npm:1.0.1" + dependencies: + "@babel/plugin-syntax-async-generators": "npm:^7.8.4" + "@babel/plugin-syntax-bigint": "npm:^7.8.3" + "@babel/plugin-syntax-class-properties": "npm:^7.8.3" + "@babel/plugin-syntax-import-meta": "npm:^7.8.3" + "@babel/plugin-syntax-json-strings": "npm:^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + "@babel/plugin-syntax-numeric-separator": "npm:^7.8.3" + "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" + "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + "@babel/plugin-syntax-top-level-await": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/5ba39a3a0e6c37d25e56a4fb843be632dac98d54706d8a0933f9bcb1a07987a96d55c2b5a6c11788a74063fb2534fe68c1f1dbb6c93626850c785e0938495627 + languageName: node + linkType: hard + +"babel-preset-jest@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-preset-jest@npm:29.6.3" + dependencies: + babel-plugin-jest-hoist: "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/ec5fd0276b5630b05f0c14bb97cc3815c6b31600c683ebb51372e54dcb776cff790bdeeabd5b8d01ede375a040337ccbf6a3ccd68d3a34219125945e167ad943 + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + languageName: node + linkType: hard + +"bcrypt@npm:^5.1.1": + version: 5.1.1 + resolution: "bcrypt@npm:5.1.1" + dependencies: + "@mapbox/node-pre-gyp": "npm:^1.0.11" + node-addon-api: "npm:^5.0.0" + checksum: 10c0/743231158c866bddc46f25eb8e9617fe38bc1a6f5f3052aba35e361d349b7f8fb80e96b45c48a4c23c45c29967ccd11c81cf31166454fc0ab019801c336cab40 + languageName: node + linkType: hard + +"better-sqlite3@npm:^11.7.0": + version: 11.7.0 + resolution: "better-sqlite3@npm:11.7.0" + dependencies: + bindings: "npm:^1.5.0" + node-gyp: "npm:latest" + prebuild-install: "npm:^7.1.1" + checksum: 10c0/66e78fb7e12f55dd78469efec7f6fcf69079e149e974be9ea24befac7c67b8fe0e23a419cae412ac4ea025c73841d8d54bd222eece1c007485e3f6bd56fd1c94 + languageName: node + linkType: hard + +"bindings@npm:^1.5.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: "npm:1.0.0" + checksum: 10c0/3dab2491b4bb24124252a91e656803eac24292473e56554e35bbfe3cc1875332cfa77600c3bac7564049dc95075bf6fcc63a4609920ff2d64d0fe405fcf0d4ba + languageName: node + linkType: hard + +"bl@npm:^4.0.3": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f + languageName: node + linkType: hard + +"body-parser@npm:1.20.3, body-parser@npm:^1.20.3": + version: 1.20.3 + resolution: "body-parser@npm:1.20.3" + dependencies: + bytes: "npm:3.1.2" + content-type: "npm:~1.0.5" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.4.24" + on-finished: "npm:2.4.1" + qs: "npm:6.13.0" + raw-body: "npm:2.5.2" + type-is: "npm:~1.6.18" + unpipe: "npm:1.0.0" + checksum: 10c0/0a9a93b7518f222885498dcecaad528cf010dd109b071bf471c93def4bfe30958b83e03496eb9c1ad4896db543d999bb62be1a3087294162a88cfa1b42c16310 + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: "npm:^1.0.0" + concat-map: "npm:0.0.1" + checksum: 10c0/695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + languageName: node + linkType: hard + +"braces@npm:^3.0.3": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: "npm:^7.1.1" + checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 + languageName: node + linkType: hard + +"browserslist@npm:^4.22.2": + version: 4.23.1 + resolution: "browserslist@npm:4.23.1" + dependencies: + caniuse-lite: "npm:^1.0.30001629" + electron-to-chromium: "npm:^1.4.796" + node-releases: "npm:^2.0.14" + update-browserslist-db: "npm:^1.0.16" + bin: + browserslist: cli.js + checksum: 10c0/eb47c7ab9d60db25ce2faca70efeb278faa7282a2f62b7f2fa2f92e5f5251cf65144244566c86559419ff4f6d78f59ea50e39911321ad91f3b27788901f1f5e9 + languageName: node + linkType: hard + +"bser@npm:2.1.1": + version: 2.1.1 + resolution: "bser@npm:2.1.1" + dependencies: + node-int64: "npm:^0.4.0" + checksum: 10c0/24d8dfb7b6d457d73f32744e678a60cc553e4ec0e9e1a01cf614b44d85c3c87e188d3cc78ef0442ce5032ee6818de20a0162ba1074725c0d08908f62ea979227 + languageName: node + linkType: hard + +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 10c0/fb2294e64d23c573d0dd1f1e7a466c3e978fe94a4e0f8183937912ca374619773bef8e2aceb854129d2efecbbc515bbd0cc78d2734a3e3031edb0888531bbc8e + languageName: node + linkType: hard + +"buffer-from@npm:^1.0.0": + version: 1.1.2 + resolution: "buffer-from@npm:1.1.2" + checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 + languageName: node + linkType: hard + +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e + languageName: node + linkType: hard + +"bytes@npm:3.1.2": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.3 + resolution: "cacache@npm:18.0.3" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: 10c0/dfda92840bb371fb66b88c087c61a74544363b37a265023223a99965b16a16bbb87661fe4948718d79df6e0cc04e85e62784fbcf1832b2a5e54ff4c46fbb45b7 + languageName: node + linkType: hard + +"call-bind@npm:^1.0.7": + version: 1.0.7 + resolution: "call-bind@npm:1.0.7" + dependencies: + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + get-intrinsic: "npm:^1.2.4" + set-function-length: "npm:^1.2.1" + checksum: 10c0/a3ded2e423b8e2a265983dba81c27e125b48eefb2655e7dfab6be597088da3d47c47976c24bc51b8fd9af1061f8f87b4ab78a314f3c77784b2ae2ba535ad8b8d + languageName: node + linkType: hard + +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301 + languageName: node + linkType: hard + +"camelcase@npm:^5.3.1": + version: 5.3.1 + resolution: "camelcase@npm:5.3.1" + checksum: 10c0/92ff9b443bfe8abb15f2b1513ca182d16126359ad4f955ebc83dc4ddcc4ef3fdd2c078bc223f2673dc223488e75c99b16cc4d056624374b799e6a1555cf61b23 + languageName: node + linkType: hard + +"camelcase@npm:^6.2.0": + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 10c0/0d701658219bd3116d12da3eab31acddb3f9440790c0792e0d398f0a520a6a4058018e546862b6fba89d7ae990efaeb97da71e1913e9ebf5a8b5621a3d55c710 + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.30001629": + version: 1.0.30001636 + resolution: "caniuse-lite@npm:1.0.30001636" + checksum: 10c0/e5f965b4da7bae1531fd9f93477d015729ff9e3fa12670ead39a9e6cdc4c43e62c272d47857c5cc332e7b02d697cb3f2f965a1030870ac7476da60c2fc81ee94 + languageName: node + linkType: hard + +"chalk@npm:^2.4.2": + version: 2.4.2 + resolution: "chalk@npm:2.4.2" + dependencies: + ansi-styles: "npm:^3.2.1" + escape-string-regexp: "npm:^1.0.5" + supports-color: "npm:^5.3.0" + checksum: 10c0/e6543f02ec877732e3a2d1c3c3323ddb4d39fbab687c23f526e25bd4c6a9bf3b83a696e8c769d078e04e5754921648f7821b2a2acfd16c550435fd630026e073 + languageName: node + linkType: hard + +"chalk@npm:^4.0.0, chalk@npm:^4.1.2": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880 + languageName: node + linkType: hard + +"char-regex@npm:^1.0.2": + version: 1.0.2 + resolution: "char-regex@npm:1.0.2" + checksum: 10c0/57a09a86371331e0be35d9083ba429e86c4f4648ecbe27455dbfb343037c16ee6fdc7f6b61f433a57cc5ded5561d71c56a150e018f40c2ffb7bc93a26dae341e + languageName: node + linkType: hard + +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10c0/ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + languageName: node + linkType: hard + +"ci-info@npm:^3.2.0": + version: 3.9.0 + resolution: "ci-info@npm:3.9.0" + checksum: 10c0/6f0109e36e111684291d46123d491bc4e7b7a1934c3a20dea28cba89f1d4a03acd892f5f6a81ed3855c38647e285a150e3c9ba062e38943bef57fee6c1554c3a + languageName: node + linkType: hard + +"cjs-module-lexer@npm:^1.0.0": + version: 1.3.1 + resolution: "cjs-module-lexer@npm:1.3.1" + checksum: 10c0/cd98fbf3c7f4272fb0ebf71d08d0c54bc75ce0e30b9d186114e15b4ba791f3d310af65a339eea2a0318599af2818cdd8886d353b43dfab94468f72987397ad16 + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5 + languageName: node + linkType: hard + +"co@npm:^4.6.0": + version: 4.6.0 + resolution: "co@npm:4.6.0" + checksum: 10c0/c0e85ea0ca8bf0a50cbdca82efc5af0301240ca88ebe3644a6ffb8ffe911f34d40f8fbcf8f1d52c5ddd66706abd4d3bfcd64259f1e8e2371d4f47573b0dc8c28 + languageName: node + linkType: hard + +"collect-v8-coverage@npm:^1.0.0": + version: 1.0.2 + resolution: "collect-v8-coverage@npm:1.0.2" + checksum: 10c0/ed7008e2e8b6852c5483b444a3ae6e976e088d4335a85aa0a9db2861c5f1d31bd2d7ff97a60469b3388deeba661a619753afbe201279fb159b4b9548ab8269a1 + languageName: node + linkType: hard + +"color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": + version: 1.9.3 + resolution: "color-convert@npm:1.9.3" + dependencies: + color-name: "npm:1.1.3" + checksum: 10c0/5ad3c534949a8c68fca8fbc6f09068f435f0ad290ab8b2f76841b9e6af7e0bb57b98cb05b0e19fe33f5d91e5a8611ad457e5f69e0a484caad1f7487fd0e8253c + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:1.1.3": + version: 1.1.3 + resolution: "color-name@npm:1.1.3" + checksum: 10c0/566a3d42cca25b9b3cd5528cd7754b8e89c0eb646b7f214e8e2eaddb69994ac5f0557d9c175eb5d8f0ad73531140d9c47525085ee752a91a2ab15ab459caf6d6 + languageName: node + linkType: hard + +"color-name@npm:^1.0.0, color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"color-string@npm:^1.6.0": + version: 1.9.1 + resolution: "color-string@npm:1.9.1" + dependencies: + color-name: "npm:^1.0.0" + simple-swizzle: "npm:^0.2.2" + checksum: 10c0/b0bfd74c03b1f837f543898b512f5ea353f71630ccdd0d66f83028d1f0924a7d4272deb278b9aef376cacf1289b522ac3fb175e99895283645a2dc3a33af2404 + languageName: node + linkType: hard + +"color-support@npm:^1.1.2": + version: 1.1.3 + resolution: "color-support@npm:1.1.3" + bin: + color-support: bin.js + checksum: 10c0/8ffeaa270a784dc382f62d9be0a98581db43e11eee301af14734a6d089bd456478b1a8b3e7db7ca7dc5b18a75f828f775c44074020b51c05fc00e6d0992b1cc6 + languageName: node + linkType: hard + +"color@npm:^3.1.3": + version: 3.2.1 + resolution: "color@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.3" + color-string: "npm:^1.6.0" + checksum: 10c0/39345d55825884c32a88b95127d417a2c24681d8b57069413596d9fcbb721459ef9d9ec24ce3e65527b5373ce171b73e38dbcd9c830a52a6487e7f37bf00e83c + languageName: node + linkType: hard + +"colorspace@npm:1.1.x": + version: 1.1.4 + resolution: "colorspace@npm:1.1.4" + dependencies: + color: "npm:^3.1.3" + text-hex: "npm:1.0.x" + checksum: 10c0/af5f91ff7f8e146b96e439ac20ed79b197210193bde721b47380a75b21751d90fa56390c773bb67c0aedd34ff85091883a437ab56861c779bd507d639ba7e123 + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 + languageName: node + linkType: hard + +"commander@npm:^2.20.3": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: 10c0/74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288 + languageName: node + linkType: hard + +"component-emitter@npm:^1.3.0": + version: 1.3.1 + resolution: "component-emitter@npm:1.3.1" + checksum: 10c0/e4900b1b790b5e76b8d71b328da41482118c0f3523a516a41be598dc2785a07fd721098d9bf6e22d89b19f4fa4e1025160dc00317ea111633a3e4f75c2b86032 + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f + languageName: node + linkType: hard + +"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": + version: 1.1.0 + resolution: "console-control-strings@npm:1.1.0" + checksum: 10c0/7ab51d30b52d461412cd467721bb82afe695da78fff8f29fe6f6b9cbaac9a2328e27a22a966014df9532100f6dd85370460be8130b9c677891ba36d96a343f50 + languageName: node + linkType: hard + +"content-disposition@npm:0.5.4": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: "npm:5.2.1" + checksum: 10c0/bac0316ebfeacb8f381b38285dc691c9939bf0a78b0b7c2d5758acadad242d04783cee5337ba7d12a565a19075af1b3c11c728e1e4946de73c6ff7ce45f3f1bb + languageName: node + linkType: hard + +"content-type@npm:~1.0.4, content-type@npm:~1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af + languageName: node + linkType: hard + +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 10c0/8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b + languageName: node + linkType: hard + +"cookie-signature@npm:1.0.6": + version: 1.0.6 + resolution: "cookie-signature@npm:1.0.6" + checksum: 10c0/b36fd0d4e3fef8456915fcf7742e58fbfcc12a17a018e0eb9501c9d5ef6893b596466f03b0564b81af29ff2538fd0aa4b9d54fe5ccbfb4c90ea50ad29fe2d221 + languageName: node + linkType: hard + +"cookie@npm:0.6.0": + version: 0.6.0 + resolution: "cookie@npm:0.6.0" + checksum: 10c0/f2318b31af7a31b4ddb4a678d024514df5e705f9be5909a192d7f116cfb6d45cbacf96a473fa733faa95050e7cff26e7832bb3ef94751592f1387b71c8956686 + languageName: node + linkType: hard + +"cookiejar@npm:^2.1.4": + version: 2.1.4 + resolution: "cookiejar@npm:2.1.4" + checksum: 10c0/2dae55611c6e1678f34d93984cbd4bda58f4fe3e5247cc4993f4a305cd19c913bbaf325086ed952e892108115073a747596453d3dc1c34947f47f731818b8ad1 + languageName: node + linkType: hard + +"cors@npm:^2.8.5": + version: 2.8.5 + resolution: "cors@npm:2.8.5" + dependencies: + object-assign: "npm:^4" + vary: "npm:^1" + checksum: 10c0/373702b7999409922da80de4a61938aabba6929aea5b6fd9096fefb9e8342f626c0ebd7507b0e8b0b311380744cc985f27edebc0a26e0ddb784b54e1085de761 + languageName: node + linkType: hard + +"create-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "create-jest@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + prompts: "npm:^2.0.1" + bin: + create-jest: bin/create-jest.js + checksum: 10c0/e7e54c280692470d3398f62a6238fd396327e01c6a0757002833f06d00afc62dd7bfe04ff2b9cd145264460e6b4d1eb8386f2925b7e567f97939843b7b0e812f + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 + languageName: node + linkType: hard + +"date-fns@npm:^2.30.0": + version: 2.30.0 + resolution: "date-fns@npm:2.30.0" + dependencies: + "@babel/runtime": "npm:^7.21.0" + checksum: 10c0/e4b521fbf22bc8c3db332bbfb7b094fd3e7627de0259a9d17c7551e2d2702608a7307a449206065916538e384f37b181565447ce2637ae09828427aed9cb5581 + languageName: node + linkType: hard + +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: 10c0/e2023b905e8cfe2eb8444fb558562b524807a51cdfe712570f360f873271600b5c94aebffaf11efb285e2c072264a7cf243eadb68f3eba0f8cc85fb86cd25df6 + languageName: node + linkType: hard + +"dayjs@npm:^1.11.3": + version: 1.11.11 + resolution: "dayjs@npm:1.11.11" + checksum: 10c0/0131d10516b9945f05a57e13f4af49a6814de5573a494824e103131a3bbe4cc470b1aefe8e17e51f9a478a22cd116084be1ee5725cedb66ec4c3f9091202dc4b + languageName: node + linkType: hard + +"debug@npm:2.6.9": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: "npm:2.0.0" + checksum: 10c0/121908fb839f7801180b69a7e218a40b5a0b718813b886b7d6bdb82001b931c938e2941d1e4450f33a1b1df1da653f5f7a0440c197f29fbf8a6e9d45ff6ef589 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": + version: 4.3.5 + resolution: "debug@npm:4.3.5" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/082c375a2bdc4f4469c99f325ff458adad62a3fc2c482d59923c260cb08152f34e2659f72b3767db8bb2f21ca81a60a42d1019605a412132d7b9f59363a005cc + languageName: node + linkType: hard + +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: "npm:^3.1.0" + checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e + languageName: node + linkType: hard + +"dedent@npm:^1.0.0": + version: 1.5.3 + resolution: "dedent@npm:1.5.3" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 10c0/d94bde6e6f780be4da4fd760288fcf755ec368872f4ac5218197200d86430aeb8d90a003a840bff1c20221188e3f23adced0119cb811c6873c70d0ac66d12832 + languageName: node + linkType: hard + +"deep-extend@npm:^0.6.0": + version: 0.6.0 + resolution: "deep-extend@npm:0.6.0" + checksum: 10c0/1c6b0abcdb901e13a44c7d699116d3d4279fdb261983122a3783e7273844d5f2537dc2e1c454a23fcf645917f93fbf8d07101c1d03c015a87faa662755212566 + languageName: node + linkType: hard + +"deep-is@npm:^0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: 10c0/7f0ee496e0dff14a573dc6127f14c95061b448b87b995fc96c017ce0a1e66af1675e73f1d6064407975bc4ea6ab679497a29fff7b5b9c4e99cb10797c1ad0b4c + languageName: node + linkType: hard + +"deepmerge@npm:^4.2.2": + version: 4.3.1 + resolution: "deepmerge@npm:4.3.1" + checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044 + languageName: node + linkType: hard + +"define-data-property@npm:^1.1.4": + version: 1.1.4 + resolution: "define-data-property@npm:1.1.4" + dependencies: + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.0.1" + checksum: 10c0/dea0606d1483eb9db8d930d4eac62ca0fa16738b0b3e07046cddfacf7d8c868bbe13fa0cb263eb91c7d0d527960dc3f2f2471a69ed7816210307f6744fe62e37 + languageName: node + linkType: hard + +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 + languageName: node + linkType: hard + +"delegates@npm:^1.0.0": + version: 1.0.0 + resolution: "delegates@npm:1.0.0" + checksum: 10c0/ba05874b91148e1db4bf254750c042bf2215febd23a6d3cda2e64896aef79745fbd4b9996488bd3cafb39ce19dbce0fd6e3b6665275638befffe1c9b312b91b5 + languageName: node + linkType: hard + +"depd@npm:2.0.0": + version: 2.0.0 + resolution: "depd@npm:2.0.0" + checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c + languageName: node + linkType: hard + +"destroy@npm:1.2.0": + version: 1.2.0 + resolution: "destroy@npm:1.2.0" + checksum: 10c0/bd7633942f57418f5a3b80d5cb53898127bcf53e24cdf5d5f4396be471417671f0fee48a4ebe9a1e9defbde2a31280011af58a57e090ff822f589b443ed4e643 + languageName: node + linkType: hard + +"detect-libc@npm:^2.0.0": + version: 2.0.3 + resolution: "detect-libc@npm:2.0.3" + checksum: 10c0/88095bda8f90220c95f162bf92cad70bd0e424913e655c20578600e35b91edc261af27531cf160a331e185c0ced93944bc7e09939143225f56312d7fd800fdb7 + languageName: node + linkType: hard + +"detect-newline@npm:^3.0.0": + version: 3.1.0 + resolution: "detect-newline@npm:3.1.0" + checksum: 10c0/c38cfc8eeb9fda09febb44bcd85e467c970d4e3bf526095394e5a4f18bc26dd0cf6b22c69c1fa9969261521c593836db335c2795218f6d781a512aea2fb8209d + languageName: node + linkType: hard + +"dezalgo@npm:^1.0.4": + version: 1.0.4 + resolution: "dezalgo@npm:1.0.4" + dependencies: + asap: "npm:^2.0.0" + wrappy: "npm:1" + checksum: 10c0/8a870ed42eade9a397e6141fe5c025148a59ed52f1f28b1db5de216b4d57f0af7a257070c3af7ce3d5508c1ce9dd5009028a76f4b2cc9370dc56551d2355fad8 + languageName: node + linkType: hard + +"diff-sequences@npm:^29.6.3": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: 10c0/32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2 + languageName: node + linkType: hard + +"dir-glob@npm:^3.0.1": + version: 3.0.1 + resolution: "dir-glob@npm:3.0.1" + dependencies: + path-type: "npm:^4.0.0" + checksum: 10c0/dcac00920a4d503e38bb64001acb19df4efc14536ada475725e12f52c16777afdee4db827f55f13a908ee7efc0cb282e2e3dbaeeb98c0993dd93d1802d3bf00c + languageName: node + linkType: hard + +"doctrine@npm:^3.0.0": + version: 3.0.0 + resolution: "doctrine@npm:3.0.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520 + languageName: node + linkType: hard + +"dotenv@npm:^10.0.0": + version: 10.0.0 + resolution: "dotenv@npm:10.0.0" + checksum: 10c0/2d8d4ba64bfaff7931402aa5e8cbb8eba0acbc99fe9ae442300199af021079eafa7171ce90e150821a5cb3d74f0057721fbe7ec201a6044b68c8a7615f8c123f + languageName: node + linkType: hard + +"dotenv@npm:^16.0.0": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 10c0/48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"ecdsa-sig-formatter@npm:1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/ebfbf19d4b8be938f4dd4a83b8788385da353d63307ede301a9252f9f7f88672e76f2191618fd8edfc2f24679236064176fab0b78131b161ee73daa37125408c + languageName: node + linkType: hard + +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: 10c0/b5bb125ee93161bc16bfe6e56c6b04de5ad2aa44234d8f644813cc95d861a6910903132b05093706de2b706599367c4130eb6d170f6b46895686b95f87d017b7 + languageName: node + linkType: hard + +"electron-to-chromium@npm:^1.4.796": + version: 1.4.805 + resolution: "electron-to-chromium@npm:1.4.805" + checksum: 10c0/90594849ebe1152c1c302183be7bf51642e24626e6d0332f8c56c5ad18d9fb821135e0ed9d0fcf3ec69422d774e48e6c226362be0d8c8efe6b0849225a28d53e + languageName: node + linkType: hard + +"emittery@npm:^0.13.1": + version: 0.13.1 + resolution: "emittery@npm:0.13.1" + checksum: 10c0/1573d0ae29ab34661b6c63251ff8f5facd24ccf6a823f19417ae8ba8c88ea450325788c67f16c99edec8de4b52ce93a10fe441ece389fd156e88ee7dab9bfa35 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"enabled@npm:2.0.x": + version: 2.0.0 + resolution: "enabled@npm:2.0.0" + checksum: 10c0/3b2c2af9bc7f8b9e291610f2dde4a75cf6ee52a68f4dd585482fbdf9a55d65388940e024e56d40bb03e05ef6671f5f53021fa8b72a20e954d7066ec28166713f + languageName: node + linkType: hard + +"encodeurl@npm:~1.0.2": + version: 1.0.2 + resolution: "encodeurl@npm:1.0.2" + checksum: 10c0/f6c2387379a9e7c1156c1c3d4f9cb7bb11cf16dd4c1682e1f6746512564b053df5781029b6061296832b59fb22f459dbe250386d217c2f6e203601abb2ee0bec + languageName: node + linkType: hard + +"encodeurl@npm:~2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.4 + resolution: "end-of-stream@npm:1.4.4" + dependencies: + once: "npm:^1.4.0" + checksum: 10c0/870b423afb2d54bb8d243c63e07c170409d41e20b47eeef0727547aea5740bd6717aca45597a9f2745525667a6b804c1e7bede41f856818faee5806dd9ff3975 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"error-ex@npm:^1.3.1": + version: 1.3.2 + resolution: "error-ex@npm:1.3.2" + dependencies: + is-arrayish: "npm:^0.2.1" + checksum: 10c0/ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce + languageName: node + linkType: hard + +"es-define-property@npm:^1.0.0": + version: 1.0.0 + resolution: "es-define-property@npm:1.0.0" + dependencies: + get-intrinsic: "npm:^1.2.4" + checksum: 10c0/6bf3191feb7ea2ebda48b577f69bdfac7a2b3c9bcf97307f55fd6ef1bbca0b49f0c219a935aca506c993d8c5d8bddd937766cb760cd5e5a1071351f2df9f9aa4 + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 + languageName: node + linkType: hard + +"escalade@npm:^3.1.1, escalade@npm:^3.1.2": + version: 3.1.2 + resolution: "escalade@npm:3.1.2" + checksum: 10c0/6b4adafecd0682f3aa1cd1106b8fff30e492c7015b178bc81b2d2f75106dabea6c6d6e8508fc491bd58e597c74abb0e8e2368f943ecb9393d4162e3c2f3cf287 + languageName: node + linkType: hard + +"escape-html@npm:~1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^1.0.5": + version: 1.0.5 + resolution: "escape-string-regexp@npm:1.0.5" + checksum: 10c0/a968ad453dd0c2724e14a4f20e177aaf32bb384ab41b674a8454afe9a41c5e6fe8903323e0a1052f56289d04bd600f81278edf140b0fcc02f5cac98d0f5b5371 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^2.0.0": + version: 2.0.0 + resolution: "escape-string-regexp@npm:2.0.0" + checksum: 10c0/2530479fe8db57eace5e8646c9c2a9c80fa279614986d16dcc6bcaceb63ae77f05a851ba6c43756d816c61d7f4534baf56e3c705e3e0d884818a46808811c507 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 10c0/9497d4dd307d845bd7f75180d8188bb17ea8c151c1edbf6b6717c100e104d629dc2dfb687686181b0f4b7d732c7dfdc4d5e7a8ff72de1b0ca283a75bbb3a9cd9 + languageName: node + linkType: hard + +"eslint-plugin-prettier@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-plugin-prettier@npm:4.2.1" + dependencies: + prettier-linter-helpers: "npm:^1.0.0" + peerDependencies: + eslint: ">=7.28.0" + prettier: ">=2.0.0" + peerDependenciesMeta: + eslint-config-prettier: + optional: true + checksum: 10c0/c5e7316baeab9d96ac39c279f16686e837277e5c67a8006c6588bcff317edffdc1532fb580441eb598bc6770f6444006756b68a6575dff1cd85ebe227252d0b7 + languageName: node + linkType: hard + +"eslint-scope@npm:^5.1.1": + version: 5.1.1 + resolution: "eslint-scope@npm:5.1.1" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^4.1.1" + checksum: 10c0/d30ef9dc1c1cbdece34db1539a4933fe3f9b14e1ffb27ecc85987902ee663ad7c9473bbd49a9a03195a373741e62e2f807c4938992e019b511993d163450e70a + languageName: node + linkType: hard + +"eslint-scope@npm:^7.2.2": + version: 7.2.2 + resolution: "eslint-scope@npm:7.2.2" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 10c0/613c267aea34b5a6d6c00514e8545ef1f1433108097e857225fed40d397dd6b1809dffd11c2fde23b37ca53d7bf935fe04d2a18e6fc932b31837b6ad67e1c116 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 + languageName: node + linkType: hard + +"eslint@npm:^8.33.0": + version: 8.57.0 + resolution: "eslint@npm:8.57.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/regexpp": "npm:^4.6.1" + "@eslint/eslintrc": "npm:^2.1.4" + "@eslint/js": "npm:8.57.0" + "@humanwhocodes/config-array": "npm:^0.11.14" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@nodelib/fs.walk": "npm:^1.2.8" + "@ungap/structured-clone": "npm:^1.2.0" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.2" + debug: "npm:^4.3.2" + doctrine: "npm:^3.0.0" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^7.2.2" + eslint-visitor-keys: "npm:^3.4.3" + espree: "npm:^9.6.1" + esquery: "npm:^1.4.2" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^6.0.1" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + globals: "npm:^13.19.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + is-path-inside: "npm:^3.0.3" + js-yaml: "npm:^4.1.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + levn: "npm:^0.4.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + strip-ansi: "npm:^6.0.1" + text-table: "npm:^0.2.0" + bin: + eslint: bin/eslint.js + checksum: 10c0/00bb96fd2471039a312435a6776fe1fd557c056755eaa2b96093ef3a8508c92c8775d5f754768be6b1dddd09fdd3379ddb231eeb9b6c579ee17ea7d68000a529 + languageName: node + linkType: hard + +"espree@npm:^9.6.0, espree@npm:^9.6.1": + version: 9.6.1 + resolution: "espree@npm:9.6.1" + dependencies: + acorn: "npm:^8.9.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10c0/1a2e9b4699b715347f62330bcc76aee224390c28bb02b31a3752e9d07549c473f5f986720483c6469cf3cfb3c9d05df612ffc69eb1ee94b54b739e67de9bb460 + languageName: node + linkType: hard + +"esprima@npm:^4.0.0": + version: 4.0.1 + resolution: "esprima@npm:4.0.1" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 10c0/ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3 + languageName: node + linkType: hard + +"esquery@npm:^1.4.2": + version: 1.5.0 + resolution: "esquery@npm:1.5.0" + dependencies: + estraverse: "npm:^5.1.0" + checksum: 10c0/a084bd049d954cc88ac69df30534043fb2aee5555b56246493f42f27d1e168f00d9e5d4192e46f10290d312dc30dc7d58994d61a609c579c1219d636996f9213 + languageName: node + linkType: hard + +"esrecurse@npm:^4.3.0": + version: 4.3.0 + resolution: "esrecurse@npm:4.3.0" + dependencies: + estraverse: "npm:^5.2.0" + checksum: 10c0/81a37116d1408ded88ada45b9fb16dbd26fba3aadc369ce50fcaf82a0bac12772ebd7b24cd7b91fc66786bf2c1ac7b5f196bc990a473efff972f5cb338877cf5 + languageName: node + linkType: hard + +"estraverse@npm:^4.1.1": + version: 4.3.0 + resolution: "estraverse@npm:4.3.0" + checksum: 10c0/9cb46463ef8a8a4905d3708a652d60122a0c20bb58dec7e0e12ab0e7235123d74214fc0141d743c381813e1b992767e2708194f6f6e0f9fd00c1b4e0887b8b6d + languageName: node + linkType: hard + +"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 10c0/1ff9447b96263dec95d6d67431c5e0771eb9776427421260a3e2f0fdd5d6bd4f8e37a7338f5ad2880c9f143450c9b1e4fc2069060724570a49cf9cf0312bd107 + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 10c0/9a2fe69a41bfdade834ba7c42de4723c97ec776e40656919c62cbd13607c45e127a003f05f724a1ea55e5029a4cf2de444b13009f2af71271e42d93a637137c7 + languageName: node + linkType: hard + +"etag@npm:~1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84 + languageName: node + linkType: hard + +"execa@npm:^5.0.0": + version: 5.1.1 + resolution: "execa@npm:5.1.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^6.0.0" + human-signals: "npm:^2.1.0" + is-stream: "npm:^2.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^4.0.1" + onetime: "npm:^5.1.2" + signal-exit: "npm:^3.0.3" + strip-final-newline: "npm:^2.0.0" + checksum: 10c0/c8e615235e8de4c5addf2fa4c3da3e3aa59ce975a3e83533b4f6a71750fb816a2e79610dc5f1799b6e28976c9ae86747a36a606655bf8cb414a74d8d507b304f + languageName: node + linkType: hard + +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: 10c0/71d2ad9b36bc25bb8b104b17e830b40a08989be7f7d100b13269aaae7c3784c3e6e1e88a797e9e87523993a25ba27c8958959a554535370672cfb4d824af8989 + languageName: node + linkType: hard + +"expand-template@npm:^2.0.3": + version: 2.0.3 + resolution: "expand-template@npm:2.0.3" + checksum: 10c0/1c9e7afe9acadf9d373301d27f6a47b34e89b3391b1ef38b7471d381812537ef2457e620ae7f819d2642ce9c43b189b3583813ec395e2938319abe356a9b2f51 + languageName: node + linkType: hard + +"expect@npm:^29.0.0, expect@npm:^29.7.0": + version: 29.7.0 + resolution: "expect@npm:29.7.0" + dependencies: + "@jest/expect-utils": "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10c0/2eddeace66e68b8d8ee5f7be57f3014b19770caaf6815c7a08d131821da527fb8c8cb7b3dcd7c883d2d3d8d184206a4268984618032d1e4b16dc8d6596475d41 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 10c0/160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + languageName: node + linkType: hard + +"express-actuator@npm:1.8.4": + version: 1.8.4 + resolution: "express-actuator@npm:1.8.4" + dependencies: + dayjs: "npm:^1.11.3" + properties-reader: "npm:^2.2.0" + checksum: 10c0/216264ba977b34d59de95721924354d7b4090b62801f225eac7f861fd8e40251e215826d7b462e57b86d02143eabdff2384b50d17d207e54a61b5aaaa726ad83 + languageName: node + linkType: hard + +"express-rate-limit@npm:^6.7.0": + version: 6.11.2 + resolution: "express-rate-limit@npm:6.11.2" + peerDependencies: + express: ^4 || ^5 + checksum: 10c0/1f92107fca92423b6311ca26cb79a3d5d79ac5f5eb81791c382c2323e331da0e993249225615003e2db91b617d69ba0d428ef60d212bfabb7a83244645f10e0a + languageName: node + linkType: hard + +"express-response-size@npm:^0.0.3": + version: 0.0.3 + resolution: "express-response-size@npm:0.0.3" + dependencies: + on-headers: "npm:1.0.1" + checksum: 10c0/9203d192c7d7b48b4ea8710aa90d78522020d469963f9e83355a140eb1b13e40c03caa752b8f9d0b6bb401fe89958c5a043f28278ff514c90af8dc7d51409bc7 + languageName: node + linkType: hard + +"express-winston@npm:^4.2.0": + version: 4.2.0 + resolution: "express-winston@npm:4.2.0" + dependencies: + chalk: "npm:^2.4.2" + lodash: "npm:^4.17.21" + peerDependencies: + winston: ">=3.x <4" + checksum: 10c0/8f80993e7d7696b22a12c68ffb72ea0cd3ad980d8394073fd972162cdca116b8f506974dd504987e350cddfdbf55402871762dcb9c69f2f7feaef2df6d93ef09 + languageName: node + linkType: hard + +"express@npm:4.20.0": + version: 4.20.0 + resolution: "express@npm:4.20.0" + dependencies: + accepts: "npm:~1.3.8" + array-flatten: "npm:1.1.1" + body-parser: "npm:1.20.3" + content-disposition: "npm:0.5.4" + content-type: "npm:~1.0.4" + cookie: "npm:0.6.0" + cookie-signature: "npm:1.0.6" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + finalhandler: "npm:1.2.0" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + merge-descriptors: "npm:1.0.3" + methods: "npm:~1.1.2" + on-finished: "npm:2.4.1" + parseurl: "npm:~1.3.3" + path-to-regexp: "npm:0.1.10" + proxy-addr: "npm:~2.0.7" + qs: "npm:6.11.0" + range-parser: "npm:~1.2.1" + safe-buffer: "npm:5.2.1" + send: "npm:0.19.0" + serve-static: "npm:1.16.0" + setprototypeof: "npm:1.2.0" + statuses: "npm:2.0.1" + type-is: "npm:~1.6.18" + utils-merge: "npm:1.0.1" + vary: "npm:~1.1.2" + checksum: 10c0/626e440e9feffa3f82ebce5e7dc0ad7a74fa96079994f30048cce450f4855a258abbcabf021f691aeb72154867f0d28440a8498c62888805faf667a829fb65aa + languageName: node + linkType: hard + +"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": + version: 3.1.3 + resolution: "fast-deep-equal@npm:3.1.3" + checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 + languageName: node + linkType: hard + +"fast-diff@npm:^1.1.2": + version: 1.3.0 + resolution: "fast-diff@npm:1.3.0" + checksum: 10c0/5c19af237edb5d5effda008c891a18a585f74bf12953be57923f17a3a4d0979565fc64dbc73b9e20926b9d895f5b690c618cbb969af0cf022e3222471220ad29 + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.9": + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 10c0/42baad7b9cd40b63e42039132bde27ca2cb3a4950d0a0f9abe4639ea1aa9d3e3b40f98b1fe31cbc0cc17b664c9ea7447d911a152fa34ec5b72977b125a6fc845 + languageName: node + linkType: hard + +"fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: 10c0/7f081eb0b8a64e0057b3bb03f974b3ef00135fbf36c1c710895cd9300f13c94ba809bb3a81cf4e1b03f6e5285610a61abbd7602d0652de423144dfee5a389c9b + languageName: node + linkType: hard + +"fast-levenshtein@npm:^2.0.6": + version: 2.0.6 + resolution: "fast-levenshtein@npm:2.0.6" + checksum: 10c0/111972b37338bcb88f7d9e2c5907862c280ebf4234433b95bc611e518d192ccb2d38119c4ac86e26b668d75f7f3894f4ff5c4982899afced7ca78633b08287c4 + languageName: node + linkType: hard + +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10c0/d90ec1c963394919828872f21edaa3ad6f1dddd288d2bd4e977027afff09f5db40f94e39536d4646f7e01761d704d72d51dce5af1b93717f3489ef808f5f4e4d + languageName: node + linkType: hard + +"fastq@npm:^1.6.0": + version: 1.17.1 + resolution: "fastq@npm:1.17.1" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10c0/1095f16cea45fb3beff558bb3afa74ca7a9250f5a670b65db7ed585f92b4b48381445cd328b3d87323da81e43232b5d5978a8201bde84e0cd514310f1ea6da34 + languageName: node + linkType: hard + +"fb-watchman@npm:^2.0.0": + version: 2.0.2 + resolution: "fb-watchman@npm:2.0.2" + dependencies: + bser: "npm:2.1.1" + checksum: 10c0/feae89ac148adb8f6ae8ccd87632e62b13563e6fb114cacb5265c51f585b17e2e268084519fb2edd133872f1d47a18e6bfd7e5e08625c0d41b93149694187581 + languageName: node + linkType: hard + +"fecha@npm:^4.2.0": + version: 4.2.3 + resolution: "fecha@npm:4.2.3" + checksum: 10c0/0e895965959cf6a22bb7b00f0bf546f2783836310f510ddf63f463e1518d4c96dec61ab33fdfd8e79a71b4856a7c865478ce2ee8498d560fe125947703c9b1cf + languageName: node + linkType: hard + +"file-entry-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "file-entry-cache@npm:6.0.1" + dependencies: + flat-cache: "npm:^3.0.4" + checksum: 10c0/58473e8a82794d01b38e5e435f6feaf648e3f36fdb3a56e98f417f4efae71ad1c0d4ebd8a9a7c50c3ad085820a93fc7494ad721e0e4ebc1da3573f4e1c3c7cdd + languageName: node + linkType: hard + +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: 10c0/3b545e3a341d322d368e880e1c204ef55f1d45cdea65f7efc6c6ce9e0c4d22d802d5629320eb779d006fe59624ac17b0e848d83cc5af7cd101f206cb704f5519 + languageName: node + linkType: hard + +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 + languageName: node + linkType: hard + +"finalhandler@npm:1.2.0": + version: 1.2.0 + resolution: "finalhandler@npm:1.2.0" + dependencies: + debug: "npm:2.6.9" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + on-finished: "npm:2.4.1" + parseurl: "npm:~1.3.3" + statuses: "npm:2.0.1" + unpipe: "npm:~1.0.0" + checksum: 10c0/64b7e5ff2ad1fcb14931cd012651631b721ce657da24aedb5650ddde9378bf8e95daa451da43398123f5de161a81e79ff5affe4f9f2a6d2df4a813d6d3e254b7 + languageName: node + linkType: hard + +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": + version: 4.1.0 + resolution: "find-up@npm:4.1.0" + dependencies: + locate-path: "npm:^5.0.0" + path-exists: "npm:^4.0.0" + checksum: 10c0/0406ee89ebeefa2d507feb07ec366bebd8a6167ae74aa4e34fb4c4abd06cf782a3ce26ae4194d70706f72182841733f00551c209fe575cb00bd92104056e78c1 + languageName: node + linkType: hard + +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: "npm:^6.0.0" + path-exists: "npm:^4.0.0" + checksum: 10c0/062c5a83a9c02f53cdd6d175a37ecf8f87ea5bbff1fdfb828f04bfa021441bc7583e8ebc0872a4c1baab96221fb8a8a275a19809fb93fbc40bd69ec35634069a + languageName: node + linkType: hard + +"flat-cache@npm:^3.0.4": + version: 3.2.0 + resolution: "flat-cache@npm:3.2.0" + dependencies: + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.3" + rimraf: "npm:^3.0.2" + checksum: 10c0/b76f611bd5f5d68f7ae632e3ae503e678d205cf97a17c6ab5b12f6ca61188b5f1f7464503efae6dc18683ed8f0b41460beb48ac4b9ac63fe6201296a91ba2f75 + languageName: node + linkType: hard + +"flatted@npm:^3.2.9": + version: 3.3.1 + resolution: "flatted@npm:3.3.1" + checksum: 10c0/324166b125ee07d4ca9bcf3a5f98d915d5db4f39d711fba640a3178b959919aae1f7cfd8aabcfef5826ed8aa8a2aa14cc85b2d7d18ff638ddf4ae3df39573eaf + languageName: node + linkType: hard + +"fn.name@npm:1.x.x": + version: 1.1.0 + resolution: "fn.name@npm:1.1.0" + checksum: 10c0/8ad62aa2d4f0b2a76d09dba36cfec61c540c13a0fd72e5d94164e430f987a7ce6a743112bbeb14877c810ef500d1f73d7f56e76d029d2e3413f20d79e3460a9a + languageName: node + linkType: hard + +"follow-redirects@npm:^1.15.6": + version: 1.15.6 + resolution: "follow-redirects@npm:1.15.6" + peerDependenciesMeta: + debug: + optional: true + checksum: 10c0/9ff767f0d7be6aa6870c82ac79cf0368cd73e01bbc00e9eb1c2a16fbb198ec105e3c9b6628bb98e9f3ac66fe29a957b9645bcb9a490bb7aa0d35f908b6b85071 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.2.1 + resolution: "foreground-child@npm:3.2.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 10c0/9a53a33dbd87090e9576bef65fb4a71de60f6863a8062a7b11bc1cbe3cc86d428677d7c0b9ef61cdac11007ac580006f78bd5638618d564cfd5e6fd713d6878f + languageName: node + linkType: hard + +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + mime-types: "npm:^2.1.12" + checksum: 10c0/cb6f3ac49180be03ff07ba3ff125f9eba2ff0b277fb33c7fc47569fc5e616882c5b1c69b9904c4c4187e97dd0419dd03b134174756f296dec62041e6527e2c6e + languageName: node + linkType: hard + +"formidable@npm:^2.1.2": + version: 2.1.2 + resolution: "formidable@npm:2.1.2" + dependencies: + dezalgo: "npm:^1.0.4" + hexoid: "npm:^1.0.0" + once: "npm:^1.4.0" + qs: "npm:^6.11.0" + checksum: 10c0/efba03d11127098daa6ef54c3c0fad25693973eb902fa88ccaaa203baebe8c74d12ba0fe1e113eccf79b9172510fa337e4e107330b124fb3a8c74697b4aa2ce3 + languageName: node + linkType: hard + +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: 10c0/9b67c3fac86acdbc9ae47ba1ddd5f2f81526fa4c8226863ede5600a3f7c7416ef451f6f1e240a3cc32d0fd79fcfe6beb08fd0da454f360032bde70bf80afbb33 + languageName: node + linkType: hard + +"fresh@npm:0.5.2": + version: 0.5.2 + resolution: "fresh@npm:0.5.2" + checksum: 10c0/c6d27f3ed86cc5b601404822f31c900dd165ba63fff8152a3ef714e2012e7535027063bc67ded4cb5b3a49fa596495d46cacd9f47d6328459cf570f08b7d9e5a + languageName: node + linkType: hard + +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8 + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 + languageName: node + linkType: hard + +"fsevents@npm:^2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 + languageName: node + linkType: hard + +"gauge@npm:^3.0.0": + version: 3.0.2 + resolution: "gauge@npm:3.0.2" + dependencies: + aproba: "npm:^1.0.3 || ^2.0.0" + color-support: "npm:^1.1.2" + console-control-strings: "npm:^1.0.0" + has-unicode: "npm:^2.0.1" + object-assign: "npm:^4.1.1" + signal-exit: "npm:^3.0.0" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + wide-align: "npm:^1.1.2" + checksum: 10c0/75230ccaf216471e31025c7d5fcea1629596ca20792de50c596eb18ffb14d8404f927cd55535aab2eeecd18d1e11bd6f23ec3c2e9878d2dda1dc74bccc34b913 + languageName: node + linkType: hard + +"gensync@npm:^1.0.0-beta.2": + version: 1.0.0-beta.2 + resolution: "gensync@npm:1.0.0-beta.2" + checksum: 10c0/782aba6cba65b1bb5af3b095d96249d20edbe8df32dbf4696fd49be2583faf676173bf4809386588828e4dd76a3354fcbeb577bab1c833ccd9fc4577f26103f8 + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4": + version: 1.2.4 + resolution: "get-intrinsic@npm:1.2.4" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + has-proto: "npm:^1.0.1" + has-symbols: "npm:^1.0.3" + hasown: "npm:^2.0.0" + checksum: 10c0/0a9b82c16696ed6da5e39b1267104475c47e3a9bdbe8b509dfe1710946e38a87be70d759f4bb3cda042d76a41ef47fe769660f3b7c0d1f68750299344ffb15b7 + languageName: node + linkType: hard + +"get-package-type@npm:^0.1.0": + version: 0.1.0 + resolution: "get-package-type@npm:0.1.0" + checksum: 10c0/e34cdf447fdf1902a1f6d5af737eaadf606d2ee3518287abde8910e04159368c268568174b2e71102b87b26c2020486f126bfca9c4fb1ceb986ff99b52ecd1be + languageName: node + linkType: hard + +"get-stream@npm:^6.0.0": + version: 6.0.1 + resolution: "get-stream@npm:6.0.1" + checksum: 10c0/49825d57d3fd6964228e6200a58169464b8e8970489b3acdc24906c782fb7f01f9f56f8e6653c4a50713771d6658f7cfe051e5eb8c12e334138c9c918b296341 + languageName: node + linkType: hard + +"github-from-package@npm:0.0.0": + version: 0.0.0 + resolution: "github-from-package@npm:0.0.0" + checksum: 10c0/737ee3f52d0a27e26332cde85b533c21fcdc0b09fb716c3f8e522cfaa9c600d4a631dec9fcde179ec9d47cca89017b7848ed4d6ae6b6b78f936c06825b1fcc12 + languageName: node + linkType: hard + +"glob-parent@npm:^5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + languageName: node + linkType: hard + +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: "npm:^4.0.3" + checksum: 10c0/317034d88654730230b3f43bb7ad4f7c90257a426e872ea0bf157473ac61c99bf5d205fad8f0185f989be8d2fa6d3c7dce1645d99d545b6ea9089c39f838e7f8 + languageName: node + linkType: hard + +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.4.1 + resolution: "glob@npm:10.4.1" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/77f2900ed98b9cc2a0e1901ee5e476d664dae3cd0f1b662b8bfd4ccf00d0edc31a11595807706a274ca10e1e251411bbf2e8e976c82bed0d879a9b89343ed379 + languageName: node + linkType: hard + +"glob@npm:^7.1.3, glob@npm:^7.1.4": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.1.1" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 10c0/65676153e2b0c9095100fe7f25a778bf45608eeb32c6048cf307f579649bcc30353277b3b898a3792602c65764e5baa4f643714dfbdfd64ea271d210c7a425fe + languageName: node + linkType: hard + +"globals@npm:^11.1.0": + version: 11.12.0 + resolution: "globals@npm:11.12.0" + checksum: 10c0/758f9f258e7b19226bd8d4af5d3b0dcf7038780fb23d82e6f98932c44e239f884847f1766e8fa9cc5635ccb3204f7fa7314d4408dd4002a5e8ea827b4018f0a1 + languageName: node + linkType: hard + +"globals@npm:^13.19.0": + version: 13.24.0 + resolution: "globals@npm:13.24.0" + dependencies: + type-fest: "npm:^0.20.2" + checksum: 10c0/d3c11aeea898eb83d5ec7a99508600fbe8f83d2cf00cbb77f873dbf2bcb39428eff1b538e4915c993d8a3b3473fa71eeebfe22c9bb3a3003d1e26b1f2c8a42cd + languageName: node + linkType: hard + +"globby@npm:^11.1.0": + version: 11.1.0 + resolution: "globby@npm:11.1.0" + dependencies: + array-union: "npm:^2.1.0" + dir-glob: "npm:^3.0.1" + fast-glob: "npm:^3.2.9" + ignore: "npm:^5.2.0" + merge2: "npm:^1.4.1" + slash: "npm:^3.0.0" + checksum: 10c0/b39511b4afe4bd8a7aead3a27c4ade2b9968649abab0a6c28b1a90141b96ca68ca5db1302f7c7bd29eab66bf51e13916b8e0a3d0ac08f75e1e84a39b35691189 + languageName: node + linkType: hard + +"google-protobuf@npm:^3.12.0-rc.1": + version: 3.21.2 + resolution: "google-protobuf@npm:3.21.2" + checksum: 10c0/df20b41aad9eba4d842d69c717a4d73ac6d321084c12f524ad5eb79a47ad185323bd1b477c19565a15fd08b6eef29e475c8ac281dbc6fe547b81d8b6b99974f5 + languageName: node + linkType: hard + +"gopd@npm:^1.0.1": + version: 1.0.1 + resolution: "gopd@npm:1.0.1" + dependencies: + get-intrinsic: "npm:^1.1.3" + checksum: 10c0/505c05487f7944c552cee72087bf1567debb470d4355b1335f2c262d218ebbff805cd3715448fe29b4b380bae6912561d0467233e4165830efd28da241418c63 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: 10c0/e951259d8cd2e0d196c72ec711add7115d42eb9a8146c8eeda5b8d3ac91e5dd816b9cd68920726d9fd4490368e7ed86e9c423f40db87e2d8dfafa00fa17c3a31 + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 10c0/1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1 + languageName: node + linkType: hard + +"has-property-descriptors@npm:^1.0.2": + version: 1.0.2 + resolution: "has-property-descriptors@npm:1.0.2" + dependencies: + es-define-property: "npm:^1.0.0" + checksum: 10c0/253c1f59e80bb476cf0dde8ff5284505d90c3bdb762983c3514d36414290475fe3fd6f574929d84de2a8eec00d35cf07cb6776205ff32efd7c50719125f00236 + languageName: node + linkType: hard + +"has-proto@npm:^1.0.1": + version: 1.0.3 + resolution: "has-proto@npm:1.0.3" + checksum: 10c0/35a6989f81e9f8022c2f4027f8b48a552de714938765d019dbea6bb547bd49ce5010a3c7c32ec6ddac6e48fc546166a3583b128f5a7add8b058a6d8b4afec205 + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.3": + version: 1.0.3 + resolution: "has-symbols@npm:1.0.3" + checksum: 10c0/e6922b4345a3f37069cdfe8600febbca791c94988c01af3394d86ca3360b4b93928bbf395859158f88099cb10b19d98e3bbab7c9ff2c1bd09cf665ee90afa2c3 + languageName: node + linkType: hard + +"has-unicode@npm:^2.0.1": + version: 2.0.1 + resolution: "has-unicode@npm:2.0.1" + checksum: 10c0/ebdb2f4895c26bb08a8a100b62d362e49b2190bcfd84b76bc4be1a3bd4d254ec52d0dd9f2fbcc093fc5eb878b20c52146f9dfd33e2686ed28982187be593b47c + languageName: node + linkType: hard + +"hasown@npm:^2.0.0": + version: 2.0.2 + resolution: "hasown@npm:2.0.2" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9 + languageName: node + linkType: hard + +"hexoid@npm:^1.0.0": + version: 1.0.0 + resolution: "hexoid@npm:1.0.0" + checksum: 10c0/9c45e8ba676b9eb88455631ebceec4c829a8374a583410dc735472ab9808bf11339fcd074633c3fa30e420901b894d8a92ffd5e2e21eddd41149546e05a91f69 + languageName: node + linkType: hard + +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + languageName: node + linkType: hard + +"http-errors@npm:2.0.0": + version: 2.0.0 + resolution: "http-errors@npm:2.0.0" + dependencies: + depd: "npm:2.0.0" + inherits: "npm:2.0.4" + setprototypeof: "npm:1.2.0" + statuses: "npm:2.0.1" + toidentifier: "npm:1.0.1" + checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^5.0.0": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" + dependencies: + agent-base: "npm:6" + debug: "npm:4" + checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.4 + resolution: "https-proxy-agent@npm:7.0.4" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 10c0/bc4f7c38da32a5fc622450b6cb49a24ff596f9bd48dcedb52d2da3fa1c1a80e100fb506bd59b326c012f21c863c69b275c23de1a01d0b84db396822fdf25e52b + languageName: node + linkType: hard + +"human-signals@npm:^2.1.0": + version: 2.1.0 + resolution: "human-signals@npm:2.1.0" + checksum: 10c0/695edb3edfcfe9c8b52a76926cd31b36978782062c0ed9b1192b36bebc75c4c87c82e178dfcb0ed0fc27ca59d434198aac0bd0be18f5781ded775604db22304a + languageName: node + linkType: hard + +"iconv-lite@npm:0.4.24": + version: 0.4.24 + resolution: "iconv-lite@npm:0.4.24" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3" + checksum: 10c0/c6886a24cc00f2a059767440ec1bc00d334a89f250db8e0f7feb4961c8727118457e27c495ba94d082e51d3baca378726cd110aaf7ded8b9bbfd6a44760cf1d4 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"ieee754@npm:^1.1.13": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb + languageName: node + linkType: hard + +"ignore@npm:^5.2.0": + version: 5.3.1 + resolution: "ignore@npm:5.3.1" + checksum: 10c0/703f7f45ffb2a27fb2c5a8db0c32e7dee66b33a225d28e8db4e1be6474795f606686a6e3bcc50e1aa12f2042db4c9d4a7d60af3250511de74620fbed052ea4cd + languageName: node + linkType: hard + +"import-fresh@npm:^3.2.1": + version: 3.3.0 + resolution: "import-fresh@npm:3.3.0" + dependencies: + parent-module: "npm:^1.0.0" + resolve-from: "npm:^4.0.0" + checksum: 10c0/7f882953aa6b740d1f0e384d0547158bc86efbf2eea0f1483b8900a6f65c5a5123c2cf09b0d542cc419d0b98a759ecaeb394237e97ea427f2da221dc3cd80cc3 + languageName: node + linkType: hard + +"import-local@npm:^3.0.2": + version: 3.1.0 + resolution: "import-local@npm:3.1.0" + dependencies: + pkg-dir: "npm:^4.2.0" + resolve-cwd: "npm:^3.0.0" + bin: + import-local-fixture: fixtures/cli.js + checksum: 10c0/c67ecea72f775fe8684ca3d057e54bdb2ae28c14bf261d2607c269c18ea0da7b730924c06262eca9aed4b8ab31e31d65bc60b50e7296c85908a56e2f7d41ecd2 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: "npm:^1.3.0" + wrappy: "npm:1" + checksum: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 + languageName: node + linkType: hard + +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + languageName: node + linkType: hard + +"ini@npm:~1.3.0": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: 10c0/ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a + languageName: node + linkType: hard + +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc + languageName: node + linkType: hard + +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: 10c0/0486e775047971d3fdb5fb4f063829bac45af299ae0b82dcf3afa2145338e08290563a2a70f34b732d795ecc8311902e541a8530eeb30d75860a78ff4e94ce2a + languageName: node + linkType: hard + +"is-arrayish@npm:^0.2.1": + version: 0.2.1 + resolution: "is-arrayish@npm:0.2.1" + checksum: 10c0/e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729 + languageName: node + linkType: hard + +"is-arrayish@npm:^0.3.1": + version: 0.3.2 + resolution: "is-arrayish@npm:0.3.2" + checksum: 10c0/f59b43dc1d129edb6f0e282595e56477f98c40278a2acdc8b0a5c57097c9eff8fe55470493df5775478cf32a4dc8eaf6d3a749f07ceee5bc263a78b2434f6a54 + languageName: node + linkType: hard + +"is-core-module@npm:^2.13.0": + version: 2.13.1 + resolution: "is-core-module@npm:2.13.1" + dependencies: + hasown: "npm:^2.0.0" + checksum: 10c0/2cba9903aaa52718f11c4896dabc189bab980870aae86a62dc0d5cedb546896770ee946fb14c84b7adf0735f5eaea4277243f1b95f5cefa90054f92fbcac2518 + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-generator-fn@npm:^2.0.0": + version: 2.1.0 + resolution: "is-generator-fn@npm:2.1.0" + checksum: 10c0/2957cab387997a466cd0bf5c1b6047bd21ecb32bdcfd8996b15747aa01002c1c88731802f1b3d34ac99f4f6874b626418bd118658cf39380fe5fff32a3af9c4d + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 + languageName: node + linkType: hard + +"is-path-inside@npm:^3.0.3": + version: 3.0.3 + resolution: "is-path-inside@npm:3.0.3" + checksum: 10c0/cf7d4ac35fb96bab6a1d2c3598fe5ebb29aafb52c0aaa482b5a3ed9d8ba3edc11631e3ec2637660c44b3ce0e61a08d54946e8af30dec0b60a7c27296c68ffd05 + languageName: node + linkType: hard + +"is-stream@npm:^2.0.0": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^5.0.4": + version: 5.2.1 + resolution: "istanbul-lib-instrument@npm:5.2.1" + dependencies: + "@babel/core": "npm:^7.12.3" + "@babel/parser": "npm:^7.14.7" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^6.3.0" + checksum: 10c0/8a1bdf3e377dcc0d33ec32fe2b6ecacdb1e4358fd0eb923d4326bb11c67622c0ceb99600a680f3dad5d29c66fc1991306081e339b4d43d0b8a2ab2e1d910a6ee + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^6.0.0": + version: 6.0.2 + resolution: "istanbul-lib-instrument@npm:6.0.2" + dependencies: + "@babel/core": "npm:^7.23.9" + "@babel/parser": "npm:^7.23.9" + "@istanbuljs/schema": "npm:^0.1.3" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^7.5.4" + checksum: 10c0/405c6ac037bf8c7ee7495980b0cd5544b2c53078c10534d0c9ceeb92a9ea7dcf8510f58ccfce31336458a8fa6ccef27b570bbb602abaa8c1650f5496a807477c + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: "npm:^3.0.0" + make-dir: "npm:^4.0.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^4.0.0": + version: 4.0.1 + resolution: "istanbul-lib-source-maps@npm:4.0.1" + dependencies: + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + source-map: "npm:^0.6.1" + checksum: 10c0/19e4cc405016f2c906dff271a76715b3e881fa9faeb3f09a86cb99b8512b3a5ed19cadfe0b54c17ca0e54c1142c9c6de9330d65506e35873994e06634eebeb66 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3": + version: 3.1.7 + resolution: "istanbul-reports@npm:3.1.7" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10c0/a379fadf9cf8dc5dfe25568115721d4a7eb82fbd50b005a6672aff9c6989b20cc9312d7865814e0859cd8df58cbf664482e1d3604be0afde1f7fc3ccc1394a51 + languageName: node + linkType: hard + +"jackspeak@npm:^3.1.2": + version: 3.4.0 + resolution: "jackspeak@npm:3.4.0" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10c0/7e42d1ea411b4d57d43ea8a6afbca9224382804359cb72626d0fc45bb8db1de5ad0248283c3db45fe73e77210750d4fcc7c2b4fe5d24fda94aaa24d658295c5f + languageName: node + linkType: hard + +"jest-changed-files@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-changed-files@npm:29.7.0" + dependencies: + execa: "npm:^5.0.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + checksum: 10c0/e071384d9e2f6bb462231ac53f29bff86f0e12394c1b49ccafbad225ce2ab7da226279a8a94f421949920bef9be7ef574fd86aee22e8adfa149be73554ab828b + languageName: node + linkType: hard + +"jest-circus@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-circus@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + co: "npm:^4.6.0" + dedent: "npm:^1.0.0" + is-generator-fn: "npm:^2.0.0" + jest-each: "npm:^29.7.0" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + pure-rand: "npm:^6.0.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10c0/8d15344cf7a9f14e926f0deed64ed190c7a4fa1ed1acfcd81e4cc094d3cc5bf7902ebb7b874edc98ada4185688f90c91e1747e0dfd7ac12463b097968ae74b5e + languageName: node + linkType: hard + +"jest-cli@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-cli@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + create-jest: "npm:^29.7.0" + exit: "npm:^0.1.2" + import-local: "npm:^3.0.2" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + yargs: "npm:^17.3.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10c0/a658fd55050d4075d65c1066364595962ead7661711495cfa1dfeecf3d6d0a8ffec532f3dbd8afbb3e172dd5fd2fb2e813c5e10256e7cf2fea766314942fb43a + languageName: node + linkType: hard + +"jest-config@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-config@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/test-sequencer": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-jest: "npm:^29.7.0" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + deepmerge: "npm:^4.2.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-circus: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + parse-json: "npm:^5.2.0" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-json-comments: "npm:^3.1.1" + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: 10c0/bab23c2eda1fff06e0d104b00d6adfb1d1aabb7128441899c9bff2247bd26710b050a5364281ce8d52b46b499153bf7e3ee88b19831a8f3451f1477a0246a0f1 + languageName: node + linkType: hard + +"jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + diff-sequences: "npm:^29.6.3" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/89a4a7f182590f56f526443dde69acefb1f2f0c9e59253c61d319569856c4931eae66b8a3790c443f529267a0ddba5ba80431c585deed81827032b2b2a1fc999 + languageName: node + linkType: hard + +"jest-docblock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-docblock@npm:29.7.0" + dependencies: + detect-newline: "npm:^3.0.0" + checksum: 10c0/d932a8272345cf6b6142bb70a2bb63e0856cc0093f082821577ea5bdf4643916a98744dfc992189d2b1417c38a11fa42466f6111526bc1fb81366f56410f3be9 + languageName: node + linkType: hard + +"jest-each@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-each@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + pretty-format: "npm:^29.7.0" + checksum: 10c0/f7f9a90ebee80cc688e825feceb2613627826ac41ea76a366fa58e669c3b2403d364c7c0a74d862d469b103c843154f8456d3b1c02b487509a12afa8b59edbb4 + languageName: node + linkType: hard + +"jest-environment-node@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-environment-node@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10c0/61f04fec077f8b1b5c1a633e3612fc0c9aa79a0ab7b05600683428f1e01a4d35346c474bde6f439f9fcc1a4aa9a2861ff852d079a43ab64b02105d1004b2592b + languageName: node + linkType: hard + +"jest-get-type@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-get-type@npm:29.6.3" + checksum: 10c0/552e7a97a983d3c2d4e412a44eb7de0430ff773dd99f7500962c268d6dfbfa431d7d08f919c9d960530e5f7f78eb47f267ad9b318265e5092b3ff9ede0db7c2b + languageName: node + linkType: hard + +"jest-haste-map@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-haste-map@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/graceful-fs": "npm:^4.1.3" + "@types/node": "npm:*" + anymatch: "npm:^3.0.3" + fb-watchman: "npm:^2.0.0" + fsevents: "npm:^2.3.2" + graceful-fs: "npm:^4.2.9" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + walker: "npm:^1.0.8" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/2683a8f29793c75a4728787662972fedd9267704c8f7ef9d84f2beed9a977f1cf5e998c07b6f36ba5603f53cb010c911fe8cd0ac9886e073fe28ca66beefd30c + languageName: node + linkType: hard + +"jest-leak-detector@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-leak-detector@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/71bb9f77fc489acb842a5c7be030f2b9acb18574dc9fb98b3100fc57d422b1abc55f08040884bd6e6dbf455047a62f7eaff12aa4058f7cbdc11558718ca6a395 + languageName: node + linkType: hard + +"jest-matcher-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-matcher-utils@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/0d0e70b28fa5c7d4dce701dc1f46ae0922102aadc24ed45d594dd9b7ae0a8a6ef8b216718d1ab79e451291217e05d4d49a82666e1a3cc2b428b75cd9c933244e + languageName: node + linkType: hard + +"jest-message-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-message-util@npm:29.7.0" + dependencies: + "@babel/code-frame": "npm:^7.12.13" + "@jest/types": "npm:^29.6.3" + "@types/stack-utils": "npm:^2.0.0" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10c0/850ae35477f59f3e6f27efac5215f706296e2104af39232bb14e5403e067992afb5c015e87a9243ec4d9df38525ef1ca663af9f2f4766aa116f127247008bd22 + languageName: node + linkType: hard + +"jest-mock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-mock@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + checksum: 10c0/7b9f8349ee87695a309fe15c46a74ab04c853369e5c40952d68061d9dc3159a0f0ed73e215f81b07ee97a9faaf10aebe5877a9d6255068a0977eae6a9ff1d5ac + languageName: node + linkType: hard + +"jest-pnp-resolver@npm:^1.2.2": + version: 1.2.3 + resolution: "jest-pnp-resolver@npm:1.2.3" + peerDependencies: + jest-resolve: "*" + peerDependenciesMeta: + jest-resolve: + optional: true + checksum: 10c0/86eec0c78449a2de733a6d3e316d49461af6a858070e113c97f75fb742a48c2396ea94150cbca44159ffd4a959f743a47a8b37a792ef6fdad2cf0a5cba973fac + languageName: node + linkType: hard + +"jest-regex-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-regex-util@npm:29.6.3" + checksum: 10c0/4e33fb16c4f42111159cafe26397118dcfc4cf08bc178a67149fb05f45546a91928b820894572679d62559839d0992e21080a1527faad65daaae8743a5705a3b + languageName: node + linkType: hard + +"jest-resolve-dependencies@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve-dependencies@npm:29.7.0" + dependencies: + jest-regex-util: "npm:^29.6.3" + jest-snapshot: "npm:^29.7.0" + checksum: 10c0/b6e9ad8ae5b6049474118ea6441dfddd385b6d1fc471db0136f7c8fbcfe97137a9665e4f837a9f49f15a29a1deb95a14439b7aec812f3f99d08f228464930f0d + languageName: node + linkType: hard + +"jest-resolve@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-pnp-resolver: "npm:^1.2.2" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + resolve: "npm:^1.20.0" + resolve.exports: "npm:^2.0.0" + slash: "npm:^3.0.0" + checksum: 10c0/59da5c9c5b50563e959a45e09e2eace783d7f9ac0b5dcc6375dea4c0db938d2ebda97124c8161310082760e8ebbeff9f6b177c15ca2f57fb424f637a5d2adb47 + languageName: node + linkType: hard + +"jest-runner@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runner@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/environment": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + graceful-fs: "npm:^4.2.9" + jest-docblock: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-leak-detector: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-resolve: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + source-map-support: "npm:0.5.13" + checksum: 10c0/2194b4531068d939f14c8d3274fe5938b77fa73126aedf9c09ec9dec57d13f22c72a3b5af01ac04f5c1cf2e28d0ac0b4a54212a61b05f10b5d6b47f2a1097bb4 + languageName: node + linkType: hard + +"jest-runtime@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runtime@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/globals": "npm:^29.7.0" + "@jest/source-map": "npm:^29.6.3" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + cjs-module-lexer: "npm:^1.0.0" + collect-v8-coverage: "npm:^1.0.0" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-bom: "npm:^4.0.0" + checksum: 10c0/7cd89a1deda0bda7d0941835434e44f9d6b7bd50b5c5d9b0fc9a6c990b2d4d2cab59685ab3cb2850ed4cc37059f6de903af5a50565d7f7f1192a77d3fd6dd2a6 + languageName: node + linkType: hard + +"jest-snapshot@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-snapshot@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@babel/generator": "npm:^7.7.2" + "@babel/plugin-syntax-jsx": "npm:^7.7.2" + "@babel/plugin-syntax-typescript": "npm:^7.7.2" + "@babel/types": "npm:^7.3.3" + "@jest/expect-utils": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + chalk: "npm:^4.0.0" + expect: "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + natural-compare: "npm:^1.4.0" + pretty-format: "npm:^29.7.0" + semver: "npm:^7.5.3" + checksum: 10c0/6e9003c94ec58172b4a62864a91c0146513207bedf4e0a06e1e2ac70a4484088a2683e3a0538d8ea913bcfd53dc54a9b98a98cdfa562e7fe1d1339aeae1da570 + languageName: node + linkType: hard + +"jest-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-util@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + graceful-fs: "npm:^4.2.9" + picomatch: "npm:^2.2.3" + checksum: 10c0/bc55a8f49fdbb8f51baf31d2a4f312fb66c9db1483b82f602c9c990e659cdd7ec529c8e916d5a89452ecbcfae4949b21b40a7a59d4ffc0cd813a973ab08c8150 + languageName: node + linkType: hard + +"jest-validate@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-validate@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + camelcase: "npm:^6.2.0" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + leven: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + checksum: 10c0/a20b930480c1ed68778c739f4739dce39423131bc070cd2505ddede762a5570a256212e9c2401b7ae9ba4d7b7c0803f03c5b8f1561c62348213aba18d9dbece2 + languageName: node + linkType: hard + +"jest-watcher@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-watcher@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + jest-util: "npm:^29.7.0" + string-length: "npm:^4.0.1" + checksum: 10c0/ec6c75030562fc8f8c727cb8f3b94e75d831fc718785abfc196e1f2a2ebc9a2e38744a15147170039628a853d77a3b695561ce850375ede3a4ee6037a2574567 + languageName: node + linkType: hard + +"jest-worker@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-worker@npm:29.7.0" + dependencies: + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + merge-stream: "npm:^2.0.0" + supports-color: "npm:^8.0.0" + checksum: 10c0/5570a3a005b16f46c131968b8a5b56d291f9bbb85ff4217e31c80bd8a02e7de799e59a54b95ca28d5c302f248b54cbffde2d177c2f0f52ffcee7504c6eabf660 + languageName: node + linkType: hard + +"jest@npm:^29.3.1": + version: 29.7.0 + resolution: "jest@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + import-local: "npm:^3.0.2" + jest-cli: "npm:^29.7.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10c0/f40eb8171cf147c617cc6ada49d062fbb03b4da666cb8d39cdbfb739a7d75eea4c3ca150fb072d0d273dce0c753db4d0467d54906ad0293f59c54f9db4a09d8b + languageName: node + linkType: hard + +"jose@npm:^4.15.5": + version: 4.15.9 + resolution: "jose@npm:4.15.9" + checksum: 10c0/4ed4ddf4a029db04bd167f2215f65d7245e4dc5f36d7ac3c0126aab38d66309a9e692f52df88975d99429e357e5fd8bab340ff20baab544d17684dd1d940a0f4 + languageName: node + linkType: hard + +"js-tokens@npm:^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0" + checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed + languageName: node + linkType: hard + +"js-yaml@npm:^3.13.1": + version: 3.14.1 + resolution: "js-yaml@npm:3.14.1" + dependencies: + argparse: "npm:^1.0.7" + esprima: "npm:^4.0.0" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/6746baaaeac312c4db8e75fa22331d9a04cccb7792d126ed8ce6a0bbcfef0cedaddd0c5098fade53db067c09fe00aa1c957674b4765610a8b06a5a189e46433b + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f + languageName: node + linkType: hard + +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 + languageName: node + linkType: hard + +"jsesc@npm:^2.5.1": + version: 2.5.2 + resolution: "jsesc@npm:2.5.2" + bin: + jsesc: bin/jsesc + checksum: 10c0/dbf59312e0ebf2b4405ef413ec2b25abb5f8f4d9bc5fb8d9f90381622ebca5f2af6a6aa9a8578f65903f9e33990a6dc798edd0ce5586894bf0e9e31803a1de88 + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 10c0/0d1c91569d9588e7eef2b49b59851f297f3ab93c7b35c7c221e288099322be6b562767d11e4821da500f3219542b9afd2e54c5dc573107c1126ed1080f8e96d7 + languageName: node + linkType: hard + +"json-parse-even-better-errors@npm:^2.3.0": + version: 2.3.1 + resolution: "json-parse-even-better-errors@npm:2.3.1" + checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 + languageName: node + linkType: hard + +"json-schema-traverse@npm:^0.4.1": + version: 0.4.1 + resolution: "json-schema-traverse@npm:0.4.1" + checksum: 10c0/108fa90d4cc6f08243aedc6da16c408daf81793bf903e9fd5ab21983cda433d5d2da49e40711da016289465ec2e62e0324dcdfbc06275a607fe3233fde4942ce + languageName: node + linkType: hard + +"json-stable-stringify-without-jsonify@npm:^1.0.1": + version: 1.0.1 + resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" + checksum: 10c0/cb168b61fd4de83e58d09aaa6425ef71001bae30d260e2c57e7d09a5fd82223e2f22a042dedaab8db23b7d9ae46854b08bb1f91675a8be11c5cffebef5fb66a5 + languageName: node + linkType: hard + +"json5@npm:^2.2.3": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 10c0/5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c + languageName: node + linkType: hard + +"jwa@npm:^2.0.0": + version: 2.0.0 + resolution: "jwa@npm:2.0.0" + dependencies: + buffer-equal-constant-time: "npm:1.0.1" + ecdsa-sig-formatter: "npm:1.0.11" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/6baab823b93c038ba1d2a9e531984dcadbc04e9eb98d171f4901b7a40d2be15961a359335de1671d78cb6d987f07cbe5d350d8143255977a889160c4d90fcc3c + languageName: node + linkType: hard + +"jws@npm:^4.0.0": + version: 4.0.0 + resolution: "jws@npm:4.0.0" + dependencies: + jwa: "npm:^2.0.0" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/f1ca77ea5451e8dc5ee219cb7053b8a4f1254a79cb22417a2e1043c1eb8a569ae118c68f24d72a589e8a3dd1824697f47d6bd4fb4bebb93a3bdf53545e721661 + languageName: node + linkType: hard + +"keyv@npm:^4.5.3": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: "npm:3.0.1" + checksum: 10c0/aa52f3c5e18e16bb6324876bb8b59dd02acf782a4b789c7b2ae21107fab95fab3890ed448d4f8dba80ce05391eeac4bfabb4f02a20221342982f806fa2cf271e + languageName: node + linkType: hard + +"kleur@npm:^3.0.3": + version: 3.0.3 + resolution: "kleur@npm:3.0.3" + checksum: 10c0/cd3a0b8878e7d6d3799e54340efe3591ca787d9f95f109f28129bdd2915e37807bf8918bb295ab86afb8c82196beec5a1adcaf29042ce3f2bd932b038fe3aa4b + languageName: node + linkType: hard + +"kuler@npm:^2.0.0": + version: 2.0.0 + resolution: "kuler@npm:2.0.0" + checksum: 10c0/0a4e99d92ca373f8f74d1dc37931909c4d0d82aebc94cf2ba265771160fc12c8df34eaaac80805efbda367e2795cb1f1dd4c3d404b6b1cf38aec94035b503d2d + languageName: node + linkType: hard + +"leven@npm:^3.1.0": + version: 3.1.0 + resolution: "leven@npm:3.1.0" + checksum: 10c0/cd778ba3fbab0f4d0500b7e87d1f6e1f041507c56fdcd47e8256a3012c98aaee371d4c15e0a76e0386107af2d42e2b7466160a2d80688aaa03e66e49949f42df + languageName: node + linkType: hard + +"levn@npm:^0.4.1": + version: 0.4.1 + resolution: "levn@npm:0.4.1" + dependencies: + prelude-ls: "npm:^1.2.1" + type-check: "npm:~0.4.0" + checksum: 10c0/effb03cad7c89dfa5bd4f6989364bfc79994c2042ec5966cb9b95990e2edee5cd8969ddf42616a0373ac49fac1403437deaf6e9050fbbaa3546093a59b9ac94e + languageName: node + linkType: hard + +"lines-and-columns@npm:^1.1.6": + version: 1.2.4 + resolution: "lines-and-columns@npm:1.2.4" + checksum: 10c0/3da6ee62d4cd9f03f5dc90b4df2540fb85b352081bee77fe4bbcd12c9000ead7f35e0a38b8d09a9bb99b13223446dd8689ff3c4959807620726d788701a83d2d + languageName: node + linkType: hard + +"locate-path@npm:^5.0.0": + version: 5.0.0 + resolution: "locate-path@npm:5.0.0" + dependencies: + p-locate: "npm:^4.1.0" + checksum: 10c0/33a1c5247e87e022f9713e6213a744557a3e9ec32c5d0b5efb10aa3a38177615bf90221a5592674857039c1a0fd2063b82f285702d37b792d973e9e72ace6c59 + languageName: node + linkType: hard + +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: "npm:^5.0.0" + checksum: 10c0/d3972ab70dfe58ce620e64265f90162d247e87159b6126b01314dd67be43d50e96a50b517bce2d9452a79409c7614054c277b5232377de50416564a77ac7aad3 + languageName: node + linkType: hard + +"lodash.merge@npm:^4.6.2": + version: 4.6.2 + resolution: "lodash.merge@npm:4.6.2" + checksum: 10c0/402fa16a1edd7538de5b5903a90228aa48eb5533986ba7fa26606a49db2572bf414ff73a2c9f5d5fd36b31c46a5d5c7e1527749c07cbcf965ccff5fbdf32c506 + languageName: node + linkType: hard + +"lodash@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c + languageName: node + linkType: hard + +"logform@npm:^2.6.0, logform@npm:^2.6.1": + version: 2.6.1 + resolution: "logform@npm:2.6.1" + dependencies: + "@colors/colors": "npm:1.6.0" + "@types/triple-beam": "npm:^1.3.2" + fecha: "npm:^4.2.0" + ms: "npm:^2.1.1" + safe-stable-stringify: "npm:^2.3.1" + triple-beam: "npm:^1.3.0" + checksum: 10c0/c20019336b1da8c08adea67dd7de2b0effdc6e35289c0156722924b571df94ba9f900ef55620c56bceb07cae7cc46057c9859accdee37a131251ba34d6789bce + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.2.2 + resolution: "lru-cache@npm:10.2.2" + checksum: 10c0/402d31094335851220d0b00985084288136136992979d0e015f0f1697e15d1c86052d7d53ae86b614e5b058425606efffc6969a31a091085d7a2b80a8a1e26d6 + languageName: node + linkType: hard + +"lru-cache@npm:^5.1.1": + version: 5.1.1 + resolution: "lru-cache@npm:5.1.1" + dependencies: + yallist: "npm:^3.0.2" + checksum: 10c0/89b2ef2ef45f543011e38737b8a8622a2f8998cddf0e5437174ef8f1f70a8b9d14a918ab3e232cb3ba343b7abddffa667f0b59075b2b80e6b4d63c3de6127482 + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 + languageName: node + linkType: hard + +"make-dir@npm:^3.1.0": + version: 3.1.0 + resolution: "make-dir@npm:3.1.0" + dependencies: + semver: "npm:^6.0.0" + checksum: 10c0/56aaafefc49c2dfef02c5c95f9b196c4eb6988040cf2c712185c7fe5c99b4091591a7fc4d4eafaaefa70ff763a26f6ab8c3ff60b9e75ea19876f49b18667ecaa + languageName: node + linkType: hard + +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" + dependencies: + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" + http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" + promise-retry: "npm:^2.0.1" + ssri: "npm:^10.0.0" + checksum: 10c0/df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e + languageName: node + linkType: hard + +"makeerror@npm:1.0.12": + version: 1.0.12 + resolution: "makeerror@npm:1.0.12" + dependencies: + tmpl: "npm:1.0.5" + checksum: 10c0/b0e6e599780ce6bab49cc413eba822f7d1f0dfebd1c103eaa3785c59e43e22c59018323cf9e1708f0ef5329e94a745d163fcbb6bff8e4c6742f9be9e86f3500c + languageName: node + linkType: hard + +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: 10c0/d160f31246907e79fed398470285f21bafb45a62869dc469b1c8877f3f064f5eabc4bcc122f9479b8b605bc5c76187d7871cf84c4ee3ecd3e487da1993279928 + languageName: node + linkType: hard + +"merge-descriptors@npm:1.0.3": + version: 1.0.3 + resolution: "merge-descriptors@npm:1.0.3" + checksum: 10c0/866b7094afd9293b5ea5dcd82d71f80e51514bed33b4c4e9f516795dc366612a4cbb4dc94356e943a8a6914889a914530badff27f397191b9b75cda20b6bae93 + languageName: node + linkType: hard + +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5 + languageName: node + linkType: hard + +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 10c0/254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb + languageName: node + linkType: hard + +"methods@npm:^1.1.2, methods@npm:~1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 10c0/bdf7cc72ff0a33e3eede03708c08983c4d7a173f91348b4b1e4f47d4cdbf734433ad971e7d1e8c77247d9e5cd8adb81ea4c67b0a2db526b758b2233d7814b8b2 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.4": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8 + languageName: node + linkType: hard + +"migrate@npm:^2.0.1": + version: 2.1.0 + resolution: "migrate@npm:2.1.0" + dependencies: + chalk: "npm:^4.1.2" + commander: "npm:^2.20.3" + dateformat: "npm:^4.6.3" + dotenv: "npm:^16.0.0" + inherits: "npm:^2.0.3" + minimatch: "npm:^9.0.1" + mkdirp: "npm:^3.0.1" + slug: "npm:^8.2.2" + bin: + migrate: bin/migrate + migrate-create: bin/migrate-create + migrate-down: bin/migrate-down + migrate-init: bin/migrate-init + migrate-list: bin/migrate-list + migrate-up: bin/migrate-up + checksum: 10c0/15a3ccd14e95f6c1eed87860860ec3195910e96fa23702867ad6ddbfa802a8c93ea94672192f1c64f868cf441449ac9aaaed629a89fb09d7e139c9502ae9b5f8 + languageName: node + linkType: hard + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 + languageName: node + linkType: hard + +"mime@npm:1.6.0": + version: 1.6.0 + resolution: "mime@npm:1.6.0" + bin: + mime: cli.js + checksum: 10c0/b92cd0adc44888c7135a185bfd0dddc42c32606401c72896a842ae15da71eb88858f17669af41e498b463cd7eb998f7b48939a25b08374c7924a9c8a6f8a81b0 + languageName: node + linkType: hard + +"mime@npm:2.6.0": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: 10c0/a7f2589900d9c16e3bdf7672d16a6274df903da958c1643c9c45771f0478f3846dcb1097f31eb9178452570271361e2149310931ec705c037210fc69639c8e6c + languageName: node + linkType: hard + +"mimic-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "mimic-fn@npm:2.1.0" + checksum: 10c0/b26f5479d7ec6cc2bce275a08f146cf78f5e7b661b18114e2506dd91ec7ec47e7a25bf4360e5438094db0560bcc868079fb3b1fb3892b833c1ecbf63f80c95a4 + languageName: node + linkType: hard + +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362 + languageName: node + linkType: hard + +"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1, minimatch@npm:^9.0.4": + version: 9.0.4 + resolution: "minimatch@npm:9.0.4" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/2c16f21f50e64922864e560ff97c587d15fd491f65d92a677a344e970fe62aafdbeafe648965fa96d33c061b4d0eabfe0213466203dd793367e7f28658cf6414 + languageName: node + linkType: hard + +"minimist@npm:^1.2.0, minimist@npm:^1.2.3": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 10c0/9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + languageName: node + linkType: hard + +"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10c0/95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + languageName: node + linkType: hard + +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d + languageName: node + linkType: hard + +"ms@npm:2.0.0": + version: 2.0.0 + resolution: "ms@npm:2.0.0" + checksum: 10c0/f8fda810b39fd7255bbdc451c46286e549794fcc700dc9cd1d25658bbc4dc2563a5de6fe7c60f798a16a60c6ceb53f033cb353f493f0cf63e5199b702943159d + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 10c0/a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc + languageName: node + linkType: hard + +"ms@npm:2.1.3, ms@npm:^2.1.1": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"murmurhash@npm:^2.0.1": + version: 2.0.1 + resolution: "murmurhash@npm:2.0.1" + checksum: 10c0/f6c7cb12d6ebc9c1cfd232fe9406089e1ceb128d24245e852866ba28967271925d915140f77fef7c92ee29b13165f4537ce80a85c3d0550b1b5cdb9f8bcaa19f + languageName: node + linkType: hard + +"napi-build-utils@npm:^1.0.1": + version: 1.0.2 + resolution: "napi-build-utils@npm:1.0.2" + checksum: 10c0/37fd2cd0ff2ad20073ce78d83fd718a740d568b225924e753ae51cb69d68f330c80544d487e5e5bd18e28702ed2ca469c2424ad948becd1862c1b0209542b2e9 + languageName: node + linkType: hard + +"natural-compare-lite@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare-lite@npm:1.4.0" + checksum: 10c0/f6cef26f5044515754802c0fc475d81426f3b90fe88c20fabe08771ce1f736ce46e0397c10acb569a4dd0acb84c7f1ee70676122f95d5bfdd747af3a6c6bbaa8 + languageName: node + linkType: hard + +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 10c0/f5f9a7974bfb28a91afafa254b197f0f22c684d4a1731763dda960d2c8e375b36c7d690e0d9dc8fba774c537af14a7e979129bca23d88d052fbeb9466955e447 + languageName: node + linkType: hard + +"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + languageName: node + linkType: hard + +"node-abi@npm:^3.3.0": + version: 3.65.0 + resolution: "node-abi@npm:3.65.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/112672015d8f27d6be2f18d64569f28f5d6a15a94cc510da513c69c3e3ab5df6dac196ef13ff115a8fadb69b554974c47ef89b4f6350a2b02de2bca5c23db1e5 + languageName: node + linkType: hard + +"node-addon-api@npm:^5.0.0": + version: 5.1.0 + resolution: "node-addon-api@npm:5.1.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/0eb269786124ba6fad9df8007a149e03c199b3e5a3038125dfb3e747c2d5113d406a4e33f4de1ea600aa2339be1f137d55eba1a73ee34e5fff06c52a5c296d1d + languageName: node + linkType: hard + +"node-fetch@npm:^2.6.7": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.1.0 + resolution: "node-gyp@npm:10.1.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^3.0.0" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^4.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/9cc821111ca244a01fb7f054db7523ab0a0cd837f665267eb962eb87695d71fb1e681f9e21464cc2fd7c05530dc4c81b810bca1a88f7d7186909b74477491a3c + languageName: node + linkType: hard + +"node-int64@npm:^0.4.0": + version: 0.4.0 + resolution: "node-int64@npm:0.4.0" + checksum: 10c0/a6a4d8369e2f2720e9c645255ffde909c0fbd41c92ea92a5607fc17055955daac99c1ff589d421eee12a0d24e99f7bfc2aabfeb1a4c14742f6c099a51863f31a + languageName: node + linkType: hard + +"node-releases@npm:^2.0.14": + version: 2.0.14 + resolution: "node-releases@npm:2.0.14" + checksum: 10c0/199fc93773ae70ec9969bc6d5ac5b2bbd6eb986ed1907d751f411fef3ede0e4bfdb45ceb43711f8078bea237b6036db8b1bf208f6ff2b70c7d615afd157f3ab9 + languageName: node + linkType: hard + +"nopt@npm:^5.0.0": + version: 5.0.0 + resolution: "nopt@npm:5.0.0" + dependencies: + abbrev: "npm:1" + bin: + nopt: bin/nopt.js + checksum: 10c0/fc5c4f07155cb455bf5fc3dd149fac421c1a40fd83c6bfe83aa82b52f02c17c5e88301321318adaa27611c8a6811423d51d29deaceab5fa158b585a61a551061 + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.1 + resolution: "nopt@npm:7.2.1" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 + languageName: node + linkType: hard + +"nordigen-node@npm:^1.4.0": + version: 1.4.0 + resolution: "nordigen-node@npm:1.4.0" + dependencies: + axios: "npm:^1.2.1" + dotenv: "npm:^10.0.0" + checksum: 10c0/a04ec90480e4e65b2169d909ac9ea3044f764d59283162420d287a6b229808754dc78c758637724d63c87aa77f2237bc47543f521b0b4057ed3980d6db137e1a + languageName: node + linkType: hard + +"normalize-path@npm:^3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + languageName: node + linkType: hard + +"npm-run-path@npm:^4.0.1": + version: 4.0.1 + resolution: "npm-run-path@npm:4.0.1" + dependencies: + path-key: "npm:^3.0.0" + checksum: 10c0/6f9353a95288f8455cf64cbeb707b28826a7f29690244c1e4bb61ec573256e021b6ad6651b394eb1ccfd00d6ec50147253aba2c5fe58a57ceb111fad62c519ac + languageName: node + linkType: hard + +"npmlog@npm:^5.0.1": + version: 5.0.1 + resolution: "npmlog@npm:5.0.1" + dependencies: + are-we-there-yet: "npm:^2.0.0" + console-control-strings: "npm:^1.1.0" + gauge: "npm:^3.0.0" + set-blocking: "npm:^2.0.0" + checksum: 10c0/489ba519031013001135c463406f55491a17fc7da295c18a04937fe3a4d523fd65e88dd418a28b967ab743d913fdeba1e29838ce0ad8c75557057c481f7d49fa + languageName: node + linkType: hard + +"object-assign@npm:^4, object-assign@npm:^4.1.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 + languageName: node + linkType: hard + +"object-hash@npm:^2.2.0": + version: 2.2.0 + resolution: "object-hash@npm:2.2.0" + checksum: 10c0/1527de843926c5442ed61f8bdddfc7dc181b6497f725b0e89fcf50a55d9c803088763ed447cac85a5aa65345f1e99c2469ba679a54349ef3c4c0aeaa396a3eb9 + languageName: node + linkType: hard + +"object-inspect@npm:^1.13.1": + version: 1.13.1 + resolution: "object-inspect@npm:1.13.1" + checksum: 10c0/fad603f408e345c82e946abdf4bfd774260a5ed3e5997a0b057c44153ac32c7271ff19e3a5ae39c858da683ba045ccac2f65245c12763ce4e8594f818f4a648d + languageName: node + linkType: hard + +"oidc-token-hash@npm:^5.0.3": + version: 5.0.3 + resolution: "oidc-token-hash@npm:5.0.3" + checksum: 10c0/d0dc0551406f09577874155cc83cf69c39e4b826293d50bb6c37936698aeca17d4bcee356ab910c859e53e83f2728a2acbd041020165191353b29de51fbca615 + languageName: node + linkType: hard + +"on-finished@npm:2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: "npm:1.1.1" + checksum: 10c0/46fb11b9063782f2d9968863d9cbba33d77aa13c17f895f56129c274318b86500b22af3a160fe9995aa41317efcd22941b6eba747f718ced08d9a73afdb087b4 + languageName: node + linkType: hard + +"on-headers@npm:1.0.1": + version: 1.0.1 + resolution: "on-headers@npm:1.0.1" + checksum: 10c0/060229267db33d0f56b03f59f4a1edf50130bf51498599b1f4b1a1bcf364bd8a9e05c3a984f0d0d384e2cdcdacdf176379a17e67bf5041e84c8c5f4a1938ec82 + languageName: node + linkType: hard + +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + languageName: node + linkType: hard + +"one-time@npm:^1.0.0": + version: 1.0.0 + resolution: "one-time@npm:1.0.0" + dependencies: + fn.name: "npm:1.x.x" + checksum: 10c0/6e4887b331edbb954f4e915831cbec0a7b9956c36f4feb5f6de98c448ac02ff881fd8d9b55a6b1b55030af184c6b648f340a76eb211812f4ad8c9b4b8692fdaa + languageName: node + linkType: hard + +"onetime@npm:^5.1.2": + version: 5.1.2 + resolution: "onetime@npm:5.1.2" + dependencies: + mimic-fn: "npm:^2.1.0" + checksum: 10c0/ffcef6fbb2692c3c40749f31ea2e22677a876daea92959b8a80b521d95cca7a668c884d8b2045d1d8ee7d56796aa405c405462af112a1477594cc63531baeb8f + languageName: node + linkType: hard + +"openid-client@npm:^5.4.2": + version: 5.6.5 + resolution: "openid-client@npm:5.6.5" + dependencies: + jose: "npm:^4.15.5" + lru-cache: "npm:^6.0.0" + object-hash: "npm:^2.2.0" + oidc-token-hash: "npm:^5.0.3" + checksum: 10c0/4308dcd37a9ffb1efc2ede0bc556ae42ccc2569e71baa52a03ddfa44407bf403d4534286f6f571381c5eaa1845c609ed699a5eb0d350acfb8c3bacb72c2a6890 + languageName: node + linkType: hard + +"optionator@npm:^0.9.3": + version: 0.9.4 + resolution: "optionator@npm:0.9.4" + dependencies: + deep-is: "npm:^0.1.3" + fast-levenshtein: "npm:^2.0.6" + levn: "npm:^0.4.1" + prelude-ls: "npm:^1.2.1" + type-check: "npm:^0.4.0" + word-wrap: "npm:^1.2.5" + checksum: 10c0/4afb687a059ee65b61df74dfe87d8d6815cd6883cb8b3d5883a910df72d0f5d029821f37025e4bccf4048873dbdb09acc6d303d27b8f76b1a80dd5a7d5334675 + languageName: node + linkType: hard + +"p-limit@npm:^2.2.0": + version: 2.3.0 + resolution: "p-limit@npm:2.3.0" + dependencies: + p-try: "npm:^2.0.0" + checksum: 10c0/8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12 + languageName: node + linkType: hard + +"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: "npm:^0.1.0" + checksum: 10c0/9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a + languageName: node + linkType: hard + +"p-locate@npm:^4.1.0": + version: 4.1.0 + resolution: "p-locate@npm:4.1.0" + dependencies: + p-limit: "npm:^2.2.0" + checksum: 10c0/1b476ad69ad7f6059744f343b26d51ce091508935c1dbb80c4e0a2f397ffce0ca3a1f9f5cd3c7ce19d7929a09719d5c65fe70d8ee289c3f267cd36f2881813e9 + languageName: node + linkType: hard + +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: "npm:^3.0.2" + checksum: 10c0/2290d627ab7903b8b70d11d384fee714b797f6040d9278932754a6860845c4d3190603a0772a663c8cb5a7b21d1b16acb3a6487ebcafa9773094edc3dfe6009a + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + languageName: node + linkType: hard + +"p-try@npm:^2.0.0": + version: 2.2.0 + resolution: "p-try@npm:2.2.0" + checksum: 10c0/c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f + languageName: node + linkType: hard + +"parent-module@npm:^1.0.0": + version: 1.0.1 + resolution: "parent-module@npm:1.0.1" + dependencies: + callsites: "npm:^3.0.0" + checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556 + languageName: node + linkType: hard + +"parse-json@npm:^5.2.0": + version: 5.2.0 + resolution: "parse-json@npm:5.2.0" + dependencies: + "@babel/code-frame": "npm:^7.0.0" + error-ex: "npm:^1.3.1" + json-parse-even-better-errors: "npm:^2.3.0" + lines-and-columns: "npm:^1.1.6" + checksum: 10c0/77947f2253005be7a12d858aedbafa09c9ae39eb4863adf330f7b416ca4f4a08132e453e08de2db46459256fb66afaac5ee758b44fe6541b7cdaf9d252e59585 + languageName: node + linkType: hard + +"parseurl@npm:~1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 10c0/8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b + languageName: node + linkType: hard + +"path-is-absolute@npm:^1.0.0": + version: 1.0.1 + resolution: "path-is-absolute@npm:1.0.1" + checksum: 10c0/127da03c82172a2a50099cddbf02510c1791fc2cc5f7713ddb613a56838db1e8168b121a920079d052e0936c23005562059756d653b7c544c53185efe53be078 + languageName: node + linkType: hard + +"path-key@npm:^3.0.0, path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-parse@npm:^1.0.7": + version: 1.0.7 + resolution: "path-parse@npm:1.0.7" + checksum: 10c0/11ce261f9d294cc7a58d6a574b7f1b935842355ec66fba3c3fd79e0f036462eaf07d0aa95bb74ff432f9afef97ce1926c720988c6a7451d8a584930ae7de86e1 + languageName: node + linkType: hard + +"path-scurry@npm:^1.11.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" + dependencies: + lru-cache: "npm:^10.2.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d + languageName: node + linkType: hard + +"path-to-regexp@npm:0.1.10": + version: 0.1.10 + resolution: "path-to-regexp@npm:0.1.10" + checksum: 10c0/34196775b9113ca6df88e94c8d83ba82c0e1a2063dd33bfe2803a980da8d49b91db8104f49d5191b44ea780d46b8670ce2b7f4a5e349b0c48c6779b653f1afe4 + languageName: node + linkType: hard + +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c + languageName: node + linkType: hard + +"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1": + version: 1.0.1 + resolution: "picocolors@npm:1.0.1" + checksum: 10c0/c63cdad2bf812ef0d66c8db29583802355d4ca67b9285d846f390cc15c2f6ccb94e8cb7eb6a6e97fc5990a6d3ad4ae42d86c84d3146e667c739a4234ed50d400 + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + languageName: node + linkType: hard + +"pirates@npm:^4.0.4": + version: 4.0.6 + resolution: "pirates@npm:4.0.6" + checksum: 10c0/00d5fa51f8dded94d7429700fb91a0c1ead00ae2c7fd27089f0c5b63e6eca36197fe46384631872690a66f390c5e27198e99006ab77ae472692ab9c2ca903f36 + languageName: node + linkType: hard + +"pkg-dir@npm:^4.2.0": + version: 4.2.0 + resolution: "pkg-dir@npm:4.2.0" + dependencies: + find-up: "npm:^4.0.0" + checksum: 10c0/c56bda7769e04907a88423feb320babaed0711af8c436ce3e56763ab1021ba107c7b0cafb11cde7529f669cfc22bffcaebffb573645cbd63842ea9fb17cd7728 + languageName: node + linkType: hard + +"prebuild-install@npm:^7.1.1": + version: 7.1.2 + resolution: "prebuild-install@npm:7.1.2" + dependencies: + detect-libc: "npm:^2.0.0" + expand-template: "npm:^2.0.3" + github-from-package: "npm:0.0.0" + minimist: "npm:^1.2.3" + mkdirp-classic: "npm:^0.5.3" + napi-build-utils: "npm:^1.0.1" + node-abi: "npm:^3.3.0" + pump: "npm:^3.0.0" + rc: "npm:^1.2.7" + simple-get: "npm:^4.0.0" + tar-fs: "npm:^2.0.0" + tunnel-agent: "npm:^0.6.0" + bin: + prebuild-install: bin.js + checksum: 10c0/e64868ba9ef2068fd7264f5b03e5298a901e02a450acdb1f56258d88c09dea601eefdb3d1dfdff8513fdd230a92961712be0676192626a3b4d01ba154d48bdd3 + languageName: node + linkType: hard + +"prelude-ls@npm:^1.2.1": + version: 1.2.1 + resolution: "prelude-ls@npm:1.2.1" + checksum: 10c0/b00d617431e7886c520a6f498a2e14c75ec58f6d93ba48c3b639cf241b54232d90daa05d83a9e9b9fef6baa63cb7e1e4602c2372fea5bc169668401eb127d0cd + languageName: node + linkType: hard + +"prettier-linter-helpers@npm:^1.0.0": + version: 1.0.0 + resolution: "prettier-linter-helpers@npm:1.0.0" + dependencies: + fast-diff: "npm:^1.1.2" + checksum: 10c0/81e0027d731b7b3697ccd2129470ed9913ecb111e4ec175a12f0fcfab0096516373bf0af2fef132af50cafb0a905b74ff57996d615f59512bb9ac7378fcc64ab + languageName: node + linkType: hard + +"prettier@npm:^2.8.3": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: 10c0/463ea8f9a0946cd5b828d8cf27bd8b567345cf02f56562d5ecde198b91f47a76b7ac9eae0facd247ace70e927143af6135e8cf411986b8cb8478784a4d6d724a + languageName: node + linkType: hard + +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": + version: 29.7.0 + resolution: "pretty-format@npm:29.7.0" + dependencies: + "@jest/schemas": "npm:^29.6.3" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^18.0.0" + checksum: 10c0/edc5ff89f51916f036c62ed433506b55446ff739358de77207e63e88a28ca2894caac6e73dcb68166a606e51c8087d32d400473e6a9fdd2dbe743f46c9c0276f + languageName: node + linkType: hard + +"proc-log@npm:^3.0.0": + version: 3.0.0 + resolution: "proc-log@npm:3.0.0" + checksum: 10c0/f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + languageName: node + linkType: hard + +"proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10c0/17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"prompts@npm:^2.0.1": + version: 2.4.2 + resolution: "prompts@npm:2.4.2" + dependencies: + kleur: "npm:^3.0.3" + sisteransi: "npm:^1.0.5" + checksum: 10c0/16f1ac2977b19fe2cf53f8411cc98db7a3c8b115c479b2ca5c82b5527cd937aa405fa04f9a5960abeb9daef53191b53b4d13e35c1f5d50e8718c76917c5f1ea4 + languageName: node + linkType: hard + +"properties-reader@npm:^2.2.0": + version: 2.3.0 + resolution: "properties-reader@npm:2.3.0" + dependencies: + mkdirp: "npm:^1.0.4" + checksum: 10c0/f665057e3a9076c643ba1198afcc71703eda227a59913252f7ff9467ece8d29c0cf8bf14bf1abcaef71570840c32a4e257e6c39b7550451bbff1a777efcf5667 + languageName: node + linkType: hard + +"proxy-addr@npm:~2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: "npm:0.2.0" + ipaddr.js: "npm:1.9.1" + checksum: 10c0/c3eed999781a35f7fd935f398b6d8920b6fb00bbc14287bc6de78128ccc1a02c89b95b56742bf7cf0362cc333c61d138532049c7dedc7a328ef13343eff81210 + languageName: node + linkType: hard + +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.0 + resolution: "pump@npm:3.0.0" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10c0/bbdeda4f747cdf47db97428f3a135728669e56a0ae5f354a9ac5b74556556f5446a46f720a8f14ca2ece5be9b4d5d23c346db02b555f46739934cc6c093a5478 + languageName: node + linkType: hard + +"punycode@npm:^2.1.0": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 + languageName: node + linkType: hard + +"pure-rand@npm:^6.0.0": + version: 6.1.0 + resolution: "pure-rand@npm:6.1.0" + checksum: 10c0/1abe217897bf74dcb3a0c9aba3555fe975023147b48db540aa2faf507aee91c03bf54f6aef0eb2bf59cc259a16d06b28eca37f0dc426d94f4692aeff02fb0e65 + languageName: node + linkType: hard + +"qs@npm:6.11.0": + version: 6.11.0 + resolution: "qs@npm:6.11.0" + dependencies: + side-channel: "npm:^1.0.4" + checksum: 10c0/4e4875e4d7c7c31c233d07a448e7e4650f456178b9dd3766b7cfa13158fdb24ecb8c4f059fa91e820dc6ab9f2d243721d071c9c0378892dcdad86e9e9a27c68f + languageName: node + linkType: hard + +"qs@npm:6.13.0": + version: 6.13.0 + resolution: "qs@npm:6.13.0" + dependencies: + side-channel: "npm:^1.0.6" + checksum: 10c0/62372cdeec24dc83a9fb240b7533c0fdcf0c5f7e0b83343edd7310f0ab4c8205a5e7c56406531f2e47e1b4878a3821d652be4192c841de5b032ca83619d8f860 + languageName: node + linkType: hard + +"qs@npm:^6.11.0": + version: 6.12.1 + resolution: "qs@npm:6.12.1" + dependencies: + side-channel: "npm:^1.0.6" + checksum: 10c0/439e6d7c6583e7c69f2cab2c39c55b97db7ce576e4c7c469082b938b7fc8746e8d547baacb69b4cd2b6666484776c3f4840ad7163a4c5326300b0afa0acdd84b + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 + languageName: node + linkType: hard + +"range-parser@npm:~1.2.1": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 10c0/96c032ac2475c8027b7a4e9fe22dc0dfe0f6d90b85e496e0f016fbdb99d6d066de0112e680805075bd989905e2123b3b3d002765149294dce0c1f7f01fcc2ea0 + languageName: node + linkType: hard + +"raw-body@npm:2.5.2": + version: 2.5.2 + resolution: "raw-body@npm:2.5.2" + dependencies: + bytes: "npm:3.1.2" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.4.24" + unpipe: "npm:1.0.0" + checksum: 10c0/b201c4b66049369a60e766318caff5cb3cc5a900efd89bdac431463822d976ad0670912c931fdbdcf5543207daf6f6833bca57aa116e1661d2ea91e12ca692c4 + languageName: node + linkType: hard + +"rc@npm:^1.2.7": + version: 1.2.8 + resolution: "rc@npm:1.2.8" + dependencies: + deep-extend: "npm:^0.6.0" + ini: "npm:~1.3.0" + minimist: "npm:^1.2.0" + strip-json-comments: "npm:~2.0.1" + bin: + rc: ./cli.js + checksum: 10c0/24a07653150f0d9ac7168e52943cc3cb4b7a22c0e43c7dff3219977c2fdca5a2760a304a029c20811a0e79d351f57d46c9bde216193a0f73978496afc2b85b15 + languageName: node + linkType: hard + +"react-is@npm:^18.0.0": + version: 18.3.1 + resolution: "react-is@npm:18.3.1" + checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072 + languageName: node + linkType: hard + +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 + languageName: node + linkType: hard + +"regenerator-runtime@npm:^0.14.0": + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 10c0/1b16eb2c4bceb1665c89de70dcb64126a22bc8eb958feef3cd68fe11ac6d2a4899b5cd1b80b0774c7c03591dc57d16631a7f69d2daa2ec98100e2f29f7ec4cc4 + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + +"resolve-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "resolve-cwd@npm:3.0.0" + dependencies: + resolve-from: "npm:^5.0.0" + checksum: 10c0/e608a3ebd15356264653c32d7ecbc8fd702f94c6703ea4ac2fb81d9c359180cba0ae2e6b71faa446631ed6145454d5a56b227efc33a2d40638ac13f8beb20ee4 + languageName: node + linkType: hard + +"resolve-from@npm:^4.0.0": + version: 4.0.0 + resolution: "resolve-from@npm:4.0.0" + checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190 + languageName: node + linkType: hard + +"resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 10c0/b21cb7f1fb746de8107b9febab60095187781137fd803e6a59a76d421444b1531b641bba5857f5dc011974d8a5c635d61cec49e6bd3b7fc20e01f0fafc4efbf2 + languageName: node + linkType: hard + +"resolve.exports@npm:^2.0.0": + version: 2.0.2 + resolution: "resolve.exports@npm:2.0.2" + checksum: 10c0/cc4cffdc25447cf34730f388dca5021156ba9302a3bad3d7f168e790dc74b2827dff603f1bc6ad3d299bac269828dca96dd77e036dc9fba6a2a1807c47ab5c98 + languageName: node + linkType: hard + +"resolve@npm:^1.20.0": + version: 1.22.8 + resolution: "resolve@npm:1.22.8" + dependencies: + is-core-module: "npm:^2.13.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/07e179f4375e1fd072cfb72ad66d78547f86e6196c4014b31cb0b8bb1db5f7ca871f922d08da0fbc05b94e9fd42206f819648fa3b5b873ebbc8e1dc68fec433a + languageName: node + linkType: hard + +"resolve@patch:resolve@npm%3A^1.20.0#optional!builtin": + version: 1.22.8 + resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.13.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/0446f024439cd2e50c6c8fa8ba77eaa8370b4180f401a96abf3d1ebc770ac51c1955e12764cde449fde3fff480a61f84388e3505ecdbab778f4bef5f8212c729 + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"reusify@npm:^1.0.4": + version: 1.0.4 + resolution: "reusify@npm:1.0.4" + checksum: 10c0/c19ef26e4e188f408922c46f7ff480d38e8dfc55d448310dfb518736b23ed2c4f547fb64a6ed5bdba92cd7e7ddc889d36ff78f794816d5e71498d645ef476107 + languageName: node + linkType: hard + +"rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 10c0/9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 + languageName: node + linkType: hard + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: 10c0/200b5ab25b5b8b7113f9901bfe3afc347e19bb7475b267d55ad0eb86a62a46d77510cb0f232507c9e5d497ebda569a08a9867d0d14f57a82ad5564d991588b39 + languageName: node + linkType: hard + +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + languageName: node + linkType: hard + +"safe-stable-stringify@npm:^2.3.1": + version: 2.4.3 + resolution: "safe-stable-stringify@npm:2.4.3" + checksum: 10c0/81dede06b8f2ae794efd868b1e281e3c9000e57b39801c6c162267eb9efda17bd7a9eafa7379e1f1cacd528d4ced7c80d7460ad26f62ada7c9e01dec61b2e768 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" + bin: + semver: bin/semver.js + checksum: 10c0/e3d79b609071caa78bcb6ce2ad81c7966a46a7431d9d58b8800cfa9cb6a63699b3899a0e4bcce36167a284578212d9ae6942b6929ba4aa5015c079a67751d42d + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4": + version: 7.6.2 + resolution: "semver@npm:7.6.2" + bin: + semver: bin/semver.js + checksum: 10c0/97d3441e97ace8be4b1976433d1c32658f6afaff09f143e52c593bae7eef33de19e3e369c88bd985ce1042c6f441c80c6803078d1de2a9988080b66684cbb30c + languageName: node + linkType: hard + +"send@npm:0.18.0": + version: 0.18.0 + resolution: "send@npm:0.18.0" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:2.0.1" + checksum: 10c0/0eb134d6a51fc13bbcb976a1f4214ea1e33f242fae046efc311e80aff66c7a43603e26a79d9d06670283a13000e51be6e0a2cb80ff0942eaf9f1cd30b7ae736a + languageName: node + linkType: hard + +"send@npm:0.19.0": + version: 0.19.0 + resolution: "send@npm:0.19.0" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:2.0.1" + checksum: 10c0/ea3f8a67a8f0be3d6bf9080f0baed6d2c51d11d4f7b4470de96a5029c598a7011c497511ccc28968b70ef05508675cebff27da9151dd2ceadd60be4e6cf845e3 + languageName: node + linkType: hard + +"serve-static@npm:1.16.0": + version: 1.16.0 + resolution: "serve-static@npm:1.16.0" + dependencies: + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + parseurl: "npm:~1.3.3" + send: "npm:0.18.0" + checksum: 10c0/d7a5beca08cc55f92998d8b87c111dd842d642404231c90c11f504f9650935da4599c13256747b0a988442a59851343271fe8e1946e03e92cd79c447b5f3ae01 + languageName: node + linkType: hard + +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 10c0/9f8c1b2d800800d0b589de1477c753492de5c1548d4ade52f57f1d1f5e04af5481554d75ce5e5c43d4004b80a3eb714398d6907027dc0534177b7539119f4454 + languageName: node + linkType: hard + +"set-function-length@npm:^1.2.1": + version: 1.2.2 + resolution: "set-function-length@npm:1.2.2" + dependencies: + define-data-property: "npm:^1.1.4" + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + get-intrinsic: "npm:^1.2.4" + gopd: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.2" + checksum: 10c0/82850e62f412a258b71e123d4ed3873fa9377c216809551192bb6769329340176f109c2eeae8c22a8d386c76739855f78e8716515c818bcaef384b51110f0f3c + languageName: node + linkType: hard + +"setprototypeof@npm:1.2.0": + version: 1.2.0 + resolution: "setprototypeof@npm:1.2.0" + checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": + version: 1.0.6 + resolution: "side-channel@npm:1.0.6" + dependencies: + call-bind: "npm:^1.0.7" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.4" + object-inspect: "npm:^1.13.1" + checksum: 10c0/d2afd163dc733cc0a39aa6f7e39bf0c436293510dbccbff446733daeaf295857dbccf94297092ec8c53e2503acac30f0b78830876f0485991d62a90e9cad305f + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"simple-concat@npm:^1.0.0": + version: 1.0.1 + resolution: "simple-concat@npm:1.0.1" + checksum: 10c0/62f7508e674414008910b5397c1811941d457dfa0db4fd5aa7fa0409eb02c3609608dfcd7508cace75b3a0bf67a2a77990711e32cd213d2c76f4fd12ee86d776 + languageName: node + linkType: hard + +"simple-get@npm:^4.0.0": + version: 4.0.1 + resolution: "simple-get@npm:4.0.1" + dependencies: + decompress-response: "npm:^6.0.0" + once: "npm:^1.3.1" + simple-concat: "npm:^1.0.0" + checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0 + languageName: node + linkType: hard + +"simple-swizzle@npm:^0.2.2": + version: 0.2.2 + resolution: "simple-swizzle@npm:0.2.2" + dependencies: + is-arrayish: "npm:^0.3.1" + checksum: 10c0/df5e4662a8c750bdba69af4e8263c5d96fe4cd0f9fe4bdfa3cbdeb45d2e869dff640beaaeb1ef0e99db4d8d2ec92f85508c269f50c972174851bc1ae5bd64308 + languageName: node + linkType: hard + +"sisteransi@npm:^1.0.5": + version: 1.0.5 + resolution: "sisteransi@npm:1.0.5" + checksum: 10c0/230ac975cca485b7f6fe2b96a711aa62a6a26ead3e6fb8ba17c5a00d61b8bed0d7adc21f5626b70d7c33c62ff4e63933017a6462942c719d1980bb0b1207ad46 + languageName: node + linkType: hard + +"slash@npm:^3.0.0": + version: 3.0.0 + resolution: "slash@npm:3.0.0" + checksum: 10c0/e18488c6a42bdfd4ac5be85b2ced3ccd0224773baae6ad42cfbb9ec74fc07f9fa8396bd35ee638084ead7a2a0818eb5e7151111544d4731ce843019dab4be47b + languageName: node + linkType: hard + +"slug@npm:^8.2.2": + version: 8.2.3 + resolution: "slug@npm:8.2.3" + bin: + slug: cli.js + checksum: 10c0/82499b57e5d2f8425e5fa366da86cf911b0d60c2d3143cadd89039fb6c5b8a3d340cd2fe26f4c85b62a6bdaa64327da5e7554ddbda176b58ee56ad932bfa3708 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.3 + resolution: "socks-proxy-agent@npm:8.0.3" + dependencies: + agent-base: "npm:^7.1.1" + debug: "npm:^4.3.4" + socks: "npm:^2.7.1" + checksum: 10c0/4950529affd8ccd6951575e21c1b7be8531b24d924aa4df3ee32df506af34b618c4e50d261f4cc603f1bfd8d426915b7d629966c8ce45b05fb5ad8c8b9a6459d + languageName: node + linkType: hard + +"socks@npm:^2.7.1": + version: 2.8.3 + resolution: "socks@npm:2.8.3" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 + languageName: node + linkType: hard + +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 10c0/137539f8c453fa0f496ea42049ab5da4569f96781f6ac8e5bfda26937be9494f4e8891f523c5f98f0e85f71b35d74127a00c46f83f6a4f54672b58d53202565e + languageName: node + linkType: hard + +"source-map@npm:^0.6.0, source-map@npm:^0.6.1": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec + languageName: node + linkType: hard + +"sprintf-js@npm:~1.0.2": + version: 1.0.3 + resolution: "sprintf-js@npm:1.0.3" + checksum: 10c0/ecadcfe4c771890140da5023d43e190b7566d9cf8b2d238600f31bec0fc653f328da4450eb04bd59a431771a8e9cc0e118f0aa3974b683a4981b4e07abc2a5bb + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.6 + resolution: "ssri@npm:10.0.6" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 + languageName: node + linkType: hard + +"stack-trace@npm:0.0.x": + version: 0.0.10 + resolution: "stack-trace@npm:0.0.10" + checksum: 10c0/9ff3dabfad4049b635a85456f927a075c9d0c210e3ea336412d18220b2a86cbb9b13ec46d6c37b70a302a4ea4d49e30e5d4944dd60ae784073f1cde778ac8f4b + languageName: node + linkType: hard + +"stack-utils@npm:^2.0.3": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" + dependencies: + escape-string-regexp: "npm:^2.0.0" + checksum: 10c0/651c9f87667e077584bbe848acaecc6049bc71979f1e9a46c7b920cad4431c388df0f51b8ad7cfd6eed3db97a2878d0fc8b3122979439ea8bac29c61c95eec8a + languageName: node + linkType: hard + +"statuses@npm:2.0.1": + version: 2.0.1 + resolution: "statuses@npm:2.0.1" + checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0 + languageName: node + linkType: hard + +"string-length@npm:^4.0.1": + version: 4.0.2 + resolution: "string-length@npm:4.0.2" + dependencies: + char-regex: "npm:^1.0.2" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/1cd77409c3d7db7bc59406f6bcc9ef0783671dcbabb23597a1177c166906ef2ee7c8290f78cae73a8aec858768f189d2cb417797df5e15ec4eb5e16b3346340c + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + +"strip-bom@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-bom@npm:4.0.0" + checksum: 10c0/26abad1172d6bc48985ab9a5f96c21e440f6e7e476686de49be813b5a59b3566dccb5c525b831ec54fe348283b47f3ffb8e080bc3f965fde12e84df23f6bb7ef + languageName: node + linkType: hard + +"strip-final-newline@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-final-newline@npm:2.0.0" + checksum: 10c0/bddf8ccd47acd85c0e09ad7375409d81653f645fda13227a9d459642277c253d877b68f2e5e4d819fe75733b0e626bac7e954c04f3236f6d196f79c94fa4a96f + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd + languageName: node + linkType: hard + +"strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 10c0/b509231cbdee45064ff4f9fd73609e2bcc4e84a4d508e9dd0f31f70356473fde18abfb5838c17d56fb236f5a06b102ef115438de0600b749e818a35fbbc48c43 + languageName: node + linkType: hard + +"superagent@npm:^8.1.2": + version: 8.1.2 + resolution: "superagent@npm:8.1.2" + dependencies: + component-emitter: "npm:^1.3.0" + cookiejar: "npm:^2.1.4" + debug: "npm:^4.3.4" + fast-safe-stringify: "npm:^2.1.1" + form-data: "npm:^4.0.0" + formidable: "npm:^2.1.2" + methods: "npm:^1.1.2" + mime: "npm:2.6.0" + qs: "npm:^6.11.0" + semver: "npm:^7.3.8" + checksum: 10c0/016416fc9c3d3a04fb648bc0efb3d3d5c9d96da00de47e4a625d9976d28c6c37ab0a7f185f2c3ec6d653ee8bb522f70fba0c1072aea7774341a6c0269a9fa77f + languageName: node + linkType: hard + +"supertest@npm:^6.3.1": + version: 6.3.4 + resolution: "supertest@npm:6.3.4" + dependencies: + methods: "npm:^1.1.2" + superagent: "npm:^8.1.2" + checksum: 10c0/f8c0b6c73b5e87da31feee6ccb36e7af766a438513cad89d6907f22c97edd83b1e765b4c8de955d5f7af4bca5fd0aaf9149ff48e21567dd290b326a8633af2a7 + languageName: node + linkType: hard + +"supports-color@npm:^5.3.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 10c0/6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10c0/afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124 + languageName: node + linkType: hard + +"supports-color@npm:^8.0.0": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89 + languageName: node + linkType: hard + +"supports-preserve-symlinks-flag@npm:^1.0.0": + version: 1.0.0 + resolution: "supports-preserve-symlinks-flag@npm:1.0.0" + checksum: 10c0/6c4032340701a9950865f7ae8ef38578d8d7053f5e10518076e6554a9381fa91bd9c6850193695c141f32b21f979c985db07265a758867bac95de05f7d8aeb39 + languageName: node + linkType: hard + +"tar-fs@npm:^2.0.0": + version: 2.1.1 + resolution: "tar-fs@npm:2.1.1" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.1.4" + checksum: 10c0/871d26a934bfb7beeae4c4d8a09689f530b565f79bd0cf489823ff0efa3705da01278160da10bb006d1a793fa0425cf316cec029b32a9159eacbeaff4965fb6d + languageName: node + linkType: hard + +"tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10c0/2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692 + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 + languageName: node + linkType: hard + +"test-exclude@npm:^6.0.0": + version: 6.0.0 + resolution: "test-exclude@npm:6.0.0" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^7.1.4" + minimatch: "npm:^3.0.4" + checksum: 10c0/019d33d81adff3f9f1bfcff18125fb2d3c65564f437d9be539270ee74b994986abb8260c7c2ce90e8f30162178b09dbbce33c6389273afac4f36069c48521f57 + languageName: node + linkType: hard + +"text-hex@npm:1.0.x": + version: 1.0.0 + resolution: "text-hex@npm:1.0.0" + checksum: 10c0/57d8d320d92c79d7c03ffb8339b825bb9637c2cbccf14304309f51d8950015c44464b6fd1b6820a3d4821241c68825634f09f5a2d9d501e84f7c6fd14376860d + languageName: node + linkType: hard + +"text-table@npm:^0.2.0": + version: 0.2.0 + resolution: "text-table@npm:0.2.0" + checksum: 10c0/02805740c12851ea5982686810702e2f14369a5f4c5c40a836821e3eefc65ffeec3131ba324692a37608294b0fd8c1e55a2dd571ffed4909822787668ddbee5c + languageName: node + linkType: hard + +"tmpl@npm:1.0.5": + version: 1.0.5 + resolution: "tmpl@npm:1.0.5" + checksum: 10c0/f935537799c2d1922cb5d6d3805f594388f75338fe7a4a9dac41504dd539704ca4db45b883b52e7b0aa5b2fd5ddadb1452bf95cd23a69da2f793a843f9451cc9 + languageName: node + linkType: hard + +"to-fast-properties@npm:^2.0.0": + version: 2.0.0 + resolution: "to-fast-properties@npm:2.0.0" + checksum: 10c0/b214d21dbfb4bce3452b6244b336806ffea9c05297148d32ebb428d5c43ce7545bdfc65a1ceb58c9ef4376a65c0cb2854d645f33961658b3e3b4f84910ddcdd7 + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 + languageName: node + linkType: hard + +"toidentifier@npm:1.0.1": + version: 1.0.1 + resolution: "toidentifier@npm:1.0.1" + checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 + languageName: node + linkType: hard + +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 + languageName: node + linkType: hard + +"triple-beam@npm:^1.3.0": + version: 1.4.1 + resolution: "triple-beam@npm:1.4.1" + checksum: 10c0/4bf1db71e14fe3ff1c3adbe3c302f1fdb553b74d7591a37323a7badb32dc8e9c290738996cbb64f8b10dc5a3833645b5d8c26221aaaaa12e50d1251c9aba2fea + languageName: node + linkType: hard + +"tslib@npm:^1.8.1": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 + languageName: node + linkType: hard + +"tsutils@npm:^3.21.0": + version: 3.21.0 + resolution: "tsutils@npm:3.21.0" + dependencies: + tslib: "npm:^1.8.1" + peerDependencies: + typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + checksum: 10c0/02f19e458ec78ead8fffbf711f834ad8ecd2cc6ade4ec0320790713dccc0a412b99e7fd907c4cda2a1dc602c75db6f12e0108e87a5afad4b2f9e90a24cabd5a2 + languageName: node + linkType: hard + +"tunnel-agent@npm:^0.6.0": + version: 0.6.0 + resolution: "tunnel-agent@npm:0.6.0" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/4c7a1b813e7beae66fdbf567a65ec6d46313643753d0beefb3c7973d66fcec3a1e7f39759f0a0b4465883499c6dc8b0750ab8b287399af2e583823e40410a17a + languageName: node + linkType: hard + +"type-check@npm:^0.4.0, type-check@npm:~0.4.0": + version: 0.4.0 + resolution: "type-check@npm:0.4.0" + dependencies: + prelude-ls: "npm:^1.2.1" + checksum: 10c0/7b3fd0ed43891e2080bf0c5c504b418fbb3e5c7b9708d3d015037ba2e6323a28152ec163bcb65212741fa5d2022e3075ac3c76440dbd344c9035f818e8ecee58 + languageName: node + linkType: hard + +"type-detect@npm:4.0.8": + version: 4.0.8 + resolution: "type-detect@npm:4.0.8" + checksum: 10c0/8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd + languageName: node + linkType: hard + +"type-fest@npm:^0.20.2": + version: 0.20.2 + resolution: "type-fest@npm:0.20.2" + checksum: 10c0/dea9df45ea1f0aaa4e2d3bed3f9a0bfe9e5b2592bddb92eb1bf06e50bcf98dbb78189668cd8bc31a0511d3fc25539b4cd5c704497e53e93e2d40ca764b10bfc3 + languageName: node + linkType: hard + +"type-fest@npm:^0.21.3": + version: 0.21.3 + resolution: "type-fest@npm:0.21.3" + checksum: 10c0/902bd57bfa30d51d4779b641c2bc403cdf1371fb9c91d3c058b0133694fcfdb817aef07a47f40faf79039eecbaa39ee9d3c532deff244f3a19ce68cea71a61e8 + languageName: node + linkType: hard + +"type-is@npm:~1.6.18": + version: 1.6.18 + resolution: "type-is@npm:1.6.18" + dependencies: + media-typer: "npm:0.3.0" + mime-types: "npm:~2.1.24" + checksum: 10c0/a23daeb538591b7efbd61ecf06b6feb2501b683ffdc9a19c74ef5baba362b4347e42f1b4ed81f5882a8c96a3bfff7f93ce3ffaf0cbbc879b532b04c97a55db9d + languageName: node + linkType: hard + +"typescript@npm:^4.9.5": + version: 4.9.5 + resolution: "typescript@npm:4.9.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/5f6cad2e728a8a063521328e612d7876e12f0d8a8390d3b3aaa452a6a65e24e9ac8ea22beb72a924fd96ea0a49ea63bb4e251fb922b12eedfb7f7a26475e5c56 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^4.9.5#optional!builtin": + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin::version=4.9.5&hash=289587" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/e3333f887c6829dfe0ab6c1dbe0dd1e3e2aeb56c66460cb85c5440c566f900c833d370ca34eb47558c0c69e78ced4bfe09b8f4f98b6de7afed9b84b8d1dd06a1 + languageName: node + linkType: hard + +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 10c0/6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10c0/cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + languageName: node + linkType: hard + +"unpipe@npm:1.0.0, unpipe@npm:~1.0.0": + version: 1.0.0 + resolution: "unpipe@npm:1.0.0" + checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c + languageName: node + linkType: hard + +"update-browserslist-db@npm:^1.0.16": + version: 1.0.16 + resolution: "update-browserslist-db@npm:1.0.16" + dependencies: + escalade: "npm:^3.1.2" + picocolors: "npm:^1.0.1" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10c0/5995399fc202adbb51567e4810e146cdf7af630a92cc969365a099150cb00597e425cc14987ca7080b09a4d0cfd2a3de53fbe72eebff171aed7f9bb81f9bf405 + languageName: node + linkType: hard + +"uri-js@npm:^4.2.2": + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" + dependencies: + punycode: "npm:^2.1.0" + checksum: 10c0/4ef57b45aa820d7ac6496e9208559986c665e49447cb072744c13b66925a362d96dd5a46c4530a6b8e203e5db5fe849369444440cb22ecfc26c679359e5dfa3c + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 + languageName: node + linkType: hard + +"utils-merge@npm:1.0.1": + version: 1.0.1 + resolution: "utils-merge@npm:1.0.1" + checksum: 10c0/02ba649de1b7ca8854bfe20a82f1dfbdda3fb57a22ab4a8972a63a34553cf7aa51bc9081cf7e001b035b88186d23689d69e71b510e610a09a4c66f68aa95b672 + languageName: node + linkType: hard + +"uuid@npm:^9.0.0": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b + languageName: node + linkType: hard + +"v8-to-istanbul@npm:^9.0.1": + version: 9.2.0 + resolution: "v8-to-istanbul@npm:9.2.0" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.12" + "@types/istanbul-lib-coverage": "npm:^2.0.1" + convert-source-map: "npm:^2.0.0" + checksum: 10c0/e691ba4dd0dea4a884e52c37dbda30cce6f9eeafe9b26721e449429c6bb0f4b6d1e33fabe7711d0f67f7a34c3bfd56c873f7375bba0b1534e6a2843ce99550e5 + languageName: node + linkType: hard + +"vary@npm:^1, vary@npm:~1.1.2": + version: 1.1.2 + resolution: "vary@npm:1.1.2" + checksum: 10c0/f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f + languageName: node + linkType: hard + +"walker@npm:^1.0.8": + version: 1.0.8 + resolution: "walker@npm:1.0.8" + dependencies: + makeerror: "npm:1.0.12" + checksum: 10c0/a17e037bccd3ca8a25a80cb850903facdfed0de4864bd8728f1782370715d679fa72e0a0f5da7c1c1379365159901e5935f35be531229da53bbfc0efdabdb48e + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 10c0/449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + languageName: node + linkType: hard + +"wide-align@npm:^1.1.2": + version: 1.1.5 + resolution: "wide-align@npm:1.1.5" + dependencies: + string-width: "npm:^1.0.2 || 2 || 3 || 4" + checksum: 10c0/1d9c2a3e36dfb09832f38e2e699c367ef190f96b82c71f809bc0822c306f5379df87bab47bed27ea99106d86447e50eb972d3c516c2f95782807a9d082fbea95 + languageName: node + linkType: hard + +"winston-transport@npm:^4.7.0": + version: 4.7.1 + resolution: "winston-transport@npm:4.7.1" + dependencies: + logform: "npm:^2.6.1" + readable-stream: "npm:^3.6.2" + triple-beam: "npm:^1.3.0" + checksum: 10c0/99b7b55cc2ef7f38988ab1717e7fd946c81b856b42a9530aef8ee725490ef2f2811f9cb06d63aa2f76a85fe99ae15b3bef10a54afde3be8b5059ce325e78481f + languageName: node + linkType: hard + +"winston@npm:^3.14.2": + version: 3.14.2 + resolution: "winston@npm:3.14.2" + dependencies: + "@colors/colors": "npm:^1.6.0" + "@dabh/diagnostics": "npm:^2.0.2" + async: "npm:^3.2.3" + is-stream: "npm:^2.0.0" + logform: "npm:^2.6.0" + one-time: "npm:^1.0.0" + readable-stream: "npm:^3.4.0" + safe-stable-stringify: "npm:^2.3.1" + stack-trace: "npm:0.0.x" + triple-beam: "npm:^1.3.0" + winston-transport: "npm:^4.7.0" + checksum: 10c0/3f8fe505ea18310982e60452f335dd2b22fdbc9b25839b6ad882971b2416d5adc94a1f1a46e24cb37d967ad01dfe5499adaf5e53575626b5ebb2a25ff30f4e1d + languageName: node + linkType: hard + +"word-wrap@npm:^1.2.5": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: 10c0/e0e4a1ca27599c92a6ca4c32260e8a92e8a44f4ef6ef93f803f8ed823f486e0889fc0b93be4db59c8d51b3064951d25e43d434e95dc8c960cc3a63d65d00ba20 + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 + languageName: node + linkType: hard + +"write-file-atomic@npm:^4.0.2": + version: 4.0.2 + resolution: "write-file-atomic@npm:4.0.2" + dependencies: + imurmurhash: "npm:^0.1.4" + signal-exit: "npm:^3.0.7" + checksum: 10c0/a2c282c95ef5d8e1c27b335ae897b5eca00e85590d92a3fd69a437919b7b93ff36a69ea04145da55829d2164e724bc62202cdb5f4b208b425aba0807889375c7 + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + +"yallist@npm:^3.0.2": + version: 3.1.1 + resolution: "yallist@npm:3.1.1" + checksum: 10c0/c66a5c46bc89af1625476f7f0f2ec3653c1a1791d2f9407cfb4c2ba812a1e1c9941416d71ba9719876530e3340a99925f697142989371b72d93b9ee628afd8c1 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 + languageName: node + linkType: hard + +"yargs@npm:^17.3.1": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + languageName: node + linkType: hard + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f + languageName: node + linkType: hard diff --git a/yarn.lock b/yarn.lock index 5f60a120657..d6d3297eacd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,7 +55,7 @@ __metadata: languageName: unknown linkType: soft -"@actual-app/web@workspace:packages/desktop-client": +"@actual-app/web@npm:*, @actual-app/web@workspace:packages/desktop-client": version: 0.0.0-use.local resolution: "@actual-app/web@workspace:packages/desktop-client" dependencies: @@ -176,6 +176,27 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.23.5": + version: 7.24.2 + resolution: "@babel/code-frame@npm:7.24.2" + dependencies: + "@babel/highlight": "npm:^7.24.2" + picocolors: "npm:^1.0.0" + checksum: 10/7db8f5b36ffa3f47a37f58f61e3d130b9ecad21961f3eede7e2a4ac2c7e4a5efb6e9d03a810c669bc986096831b6c0dfc2c3082673d93351b82359c1b03e0590 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0": + version: 7.26.2 + resolution: "@babel/code-frame@npm:7.26.2" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.25.9" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10/db2c2122af79d31ca916755331bb4bac96feb2b334cdaca5097a6b467fdd41963b89b14b6836a14f083de7ff887fc78fa1b3c10b14e743d33e12dbfe5ee3d223 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.3, @babel/compat-data@npm:^7.23.5": version: 7.23.5 resolution: "@babel/compat-data@npm:7.23.5" @@ -183,6 +204,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.25.9": + version: 7.26.2 + resolution: "@babel/compat-data@npm:7.26.2" + checksum: 10/ed9eed6b62ce803ef4a320b1dac76b0302abbb29c49dddf96f3e3207d9717eb34e299a8651bb1582e9c3346ead74b6d595ffced5b3dae718afa08b18741f8402 + languageName: node + linkType: hard + "@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.7.2, @babel/core@npm:^7.8.0": version: 7.24.5 resolution: "@babel/core@npm:7.24.5" @@ -206,7 +234,42 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.24.5, @babel/generator@npm:^7.25.6, @babel/generator@npm:^7.7.2": +"@babel/core@npm:^7.23.9": + version: 7.26.0 + resolution: "@babel/core@npm:7.26.0" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.26.0" + "@babel/generator": "npm:^7.26.0" + "@babel/helper-compilation-targets": "npm:^7.25.9" + "@babel/helper-module-transforms": "npm:^7.26.0" + "@babel/helpers": "npm:^7.26.0" + "@babel/parser": "npm:^7.26.0" + "@babel/template": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.26.0" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10/65767bfdb1f02e80d3af4f138066670ef8fdd12293de85ef151758a901c191c797e86d2e99b11c4cdfca33c72385ecaf38bbd7fa692791ec44c77763496b9b93 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.24.5, @babel/generator@npm:^7.7.2": + version: 7.24.5 + resolution: "@babel/generator@npm:7.24.5" + dependencies: + "@babel/types": "npm:^7.24.5" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^2.5.1" + checksum: 10/7a3782f1d2f824025a538444a0fce44f5b30a7b013984279561bcb3450eec91a41526533fd0b25b1a6fde627bebd0e645c0ea2aa907cc15c7f3da2d9eb71f069 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.25.6": version: 7.25.6 resolution: "@babel/generator@npm:7.25.6" dependencies: @@ -218,6 +281,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.25.9, @babel/generator@npm:^7.26.0": + version: 7.26.2 + resolution: "@babel/generator@npm:7.26.2" + dependencies: + "@babel/parser": "npm:^7.26.2" + "@babel/types": "npm:^7.26.0" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^3.0.2" + checksum: 10/71ace82b5b07a554846a003624bfab93275ccf73cdb9f1a37a4c1094bf9dc94bb677c67e8b8c939dbd6c5f0eda2e8f268aa2b0d9c3b9511072565660e717e045 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -227,6 +303,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-annotate-as-pure@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-annotate-as-pure@npm:7.25.9" + dependencies: + "@babel/types": "npm:^7.25.9" + checksum: 10/41edda10df1ae106a9b4fe617bf7c6df77db992992afd46192534f5cff29f9e49a303231733782dd65c5f9409714a529f215325569f14282046e9d3b7a1ffb6c + languageName: node + linkType: hard + "@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.22.15" @@ -249,6 +334,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-compilation-targets@npm:7.25.9" + dependencies: + "@babel/compat-data": "npm:^7.25.9" + "@babel/helper-validator-option": "npm:^7.25.9" + browserslist: "npm:^4.24.0" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10/8053fbfc21e8297ab55c8e7f9f119e4809fa7e505268691e1bedc2cf5e7a5a7de8c60ad13da2515378621b7601c42e101d2d679904da395fa3806a1edef6b92e + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.22.15": version: 7.23.10 resolution: "@babel/helper-create-class-features-plugin@npm:7.23.10" @@ -268,6 +366,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-create-class-features-plugin@npm:7.25.9" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + "@babel/helper-member-expression-to-functions": "npm:^7.25.9" + "@babel/helper-optimise-call-expression": "npm:^7.25.9" + "@babel/helper-replace-supers": "npm:^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/d1d47a7b5fd317c6cb1446b0e4f4892c19ddaa69ea0229f04ba8bea5f273fc8168441e7114ad36ff919f2d310f97310cec51adc79002e22039a7e1640ccaf248 + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.15, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.15" @@ -331,7 +446,26 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.24.3": +"@babel/helper-member-expression-to-functions@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-member-expression-to-functions@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/ef8cc1c1e600b012b312315f843226545a1a89f25d2f474ce2503fd939ca3f8585180f291a3a13efc56cf13eddc1d41a3a040eae9a521838fd59a6d04cc82490 + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.24.3": + version: 7.24.3 + resolution: "@babel/helper-module-imports@npm:7.24.3" + dependencies: + "@babel/types": "npm:^7.24.0" + checksum: 10/42fe124130b78eeb4bb6af8c094aa749712be0f4606f46716ce74bc18a5ea91c918c547c8bb2307a2e4b33f163e4ad2cb6a7b45f80448e624eae45b597ea3499 + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.16.7": version: 7.24.7 resolution: "@babel/helper-module-imports@npm:7.24.7" dependencies: @@ -341,6 +475,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-module-imports@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e090be5dee94dda6cd769972231b21ddfae988acd76b703a480ac0c96f3334557d70a965bf41245d6ee43891e7571a8b400ccf2b2be5803351375d0f4e5bcf08 + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.23.3, @babel/helper-module-transforms@npm:^7.24.5": version: 7.24.5 resolution: "@babel/helper-module-transforms@npm:7.24.5" @@ -356,6 +500,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.25.9, @babel/helper-module-transforms@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helper-module-transforms@npm:7.26.0" + dependencies: + "@babel/helper-module-imports": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/9841d2a62f61ad52b66a72d08264f23052d533afc4ce07aec2a6202adac0bfe43014c312f94feacb3291f4c5aafe681955610041ece2c276271adce3f570f2f5 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-optimise-call-expression@npm:7.22.5" @@ -365,6 +522,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-optimise-call-expression@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-optimise-call-expression@npm:7.25.9" + dependencies: + "@babel/types": "npm:^7.25.9" + checksum: 10/f09d0ad60c0715b9a60c31841b3246b47d67650c512ce85bbe24a3124f1a4d66377df793af393273bc6e1015b0a9c799626c48e53747581c1582b99167cc65dc + languageName: node + linkType: hard + "@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.22.5 resolution: "@babel/helper-plugin-utils@npm:7.22.5" @@ -372,6 +538,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-plugin-utils@npm:7.25.9" + checksum: 10/e347d87728b1ab10b6976d46403941c8f9008c045ea6d99997a7ffca7b852dc34b6171380f7b17edf94410e0857ff26f3a53d8618f11d73744db86e8ca9b8c64 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-remap-async-to-generator@npm:7.22.20" @@ -398,6 +571,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-replace-supers@npm:7.25.9" + dependencies: + "@babel/helper-member-expression-to-functions": "npm:^7.25.9" + "@babel/helper-optimise-call-expression": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/8ebf787016953e4479b99007bac735c9c860822fafc51bc3db67bc53814539888797238c81fa8b948b6da897eb7b1c1d4f04df11e501a7f0596b356be02de2ab + languageName: node + linkType: hard + "@babel/helper-simple-access@npm:^7.22.5, @babel/helper-simple-access@npm:^7.24.5": version: 7.24.5 resolution: "@babel/helper-simple-access@npm:7.24.5" @@ -407,6 +593,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-simple-access@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-simple-access@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/a16a6cfa5e8ac7144e856bcdaaf0022cf5de028fc0c56ce21dd664a6e900999a4285c587a209f2acf9de438c0d60bfb497f5f34aa34cbaf29da3e2f8d8d7feb7 + languageName: node + linkType: hard + "@babel/helper-skip-transparent-expression-wrappers@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.22.5" @@ -416,6 +612,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/fdbb5248932198bc26daa6abf0d2ac42cab9c2dbb75b7e9f40d425c8f28f09620b886d40e7f9e4e08ffc7aaa2cefe6fc2c44be7c20e81f7526634702fb615bdc + languageName: node + linkType: hard + "@babel/helper-split-export-declaration@npm:^7.22.6, @babel/helper-split-export-declaration@npm:^7.24.5": version: 7.24.5 resolution: "@babel/helper-split-export-declaration@npm:7.24.5" @@ -425,6 +631,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helper-string-parser@npm:7.24.1" + checksum: 10/04c0ede77b908b43e6124753b48bc485528112a9335f0a21a226bff1ace75bb6e64fab24c85cb4b1610ef3494dacd1cb807caeb6b79a7b36c43d48c289b35949 + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.24.8": version: 7.24.8 resolution: "@babel/helper-string-parser@npm:7.24.8" @@ -432,6 +645,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-string-parser@npm:7.25.9" + checksum: 10/c28656c52bd48e8c1d9f3e8e68ecafd09d949c57755b0d353739eb4eae7ba4f7e67e92e4036f1cd43378cc1397a2c943ed7bcaf5949b04ab48607def0258b775 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.24.5, @babel/helper-validator-identifier@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-validator-identifier@npm:7.24.7" @@ -439,6 +659,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-identifier@npm:7.25.9" + checksum: 10/3f9b649be0c2fd457fa1957b694b4e69532a668866b8a0d81eabfa34ba16dbf3107b39e0e7144c55c3c652bf773ec816af8df4a61273a2bb4eb3145ca9cf478e + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.23.5": version: 7.23.5 resolution: "@babel/helper-validator-option@npm:7.23.5" @@ -446,6 +673,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-option@npm:7.25.9" + checksum: 10/9491b2755948ebbdd68f87da907283698e663b5af2d2b1b02a2765761974b1120d5d8d49e9175b167f16f72748ffceec8c9cf62acfbee73f4904507b246e2b3d + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-wrap-function@npm:7.22.20" @@ -468,6 +702,28 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helpers@npm:7.26.0" + dependencies: + "@babel/template": "npm:^7.25.9" + "@babel/types": "npm:^7.26.0" + checksum: 10/fd4757f65d10b64cfdbf4b3adb7ea6ffff9497c53e0786452f495d1f7794da7e0898261b4db65e1c62bbb9a360d7d78a1085635c23dfc3af2ab6dcba06585f86 + languageName: node + linkType: hard + +"@babel/highlight@npm:^7.24.2": + version: 7.24.5 + resolution: "@babel/highlight@npm:7.24.5" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.24.5" + chalk: "npm:^2.4.2" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10/afde0403154ad69ecd58a98903058e776760444bf4d0363fb740a8596bc6278b72c5226637c4f6b3674d70acb1665207fe2fcecfe93a74f2f4ab033e89fd7e8c + languageName: node + linkType: hard + "@babel/highlight@npm:^7.24.7": version: 7.24.7 resolution: "@babel/highlight@npm:7.24.7" @@ -491,6 +747,26 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.0, @babel/parser@npm:^7.26.2": + version: 7.26.2 + resolution: "@babel/parser@npm:7.26.2" + dependencies: + "@babel/types": "npm:^7.26.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10/8baee43752a3678ad9f9e360ec845065eeee806f1fdc8e0f348a8a0e13eef0959dabed4a197c978896c493ea205c804d0a1187cc52e4a1ba017c7935bab4983d + languageName: node + linkType: hard + +"@babel/parser@npm:^7.24.0": + version: 7.24.5 + resolution: "@babel/parser@npm:7.24.5" + bin: + parser: ./bin/babel-parser.js + checksum: 10/f5ed1c5fd4b0045a364fb906f54fd30e2fff93a45069068b6d80d3ab2b64f5569c90fb41d39aff80fb7e925ca4d44917965a76776a3ca11924ec1fae3be5d1ea + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" @@ -646,6 +922,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-jsx@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-syntax-jsx@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/bb609d1ffb50b58f0c1bac8810d0e46a4f6c922aa171c458f3a19d66ee545d36e782d3bffbbc1fed0dc65a558bdce1caf5279316583c0fff5a2c1658982a8563 + languageName: node + linkType: hard + "@babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.22.5 resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" @@ -745,6 +1032,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-syntax-typescript@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/0e9821e8ba7d660c36c919654e4144a70546942ae184e85b8102f2322451eae102cbfadbcadd52ce077a2b44b400ee52394c616feab7b5b9f791b910e933fd33 + languageName: node + linkType: hard + "@babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.22.5 resolution: "@babel/plugin-syntax-typescript@npm:7.22.5" @@ -1049,6 +1347,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.25.9" + dependencies: + "@babel/helper-module-transforms": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-simple-access": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a7390ca999373ccdef91075f274d1ace3a5cb79f9b9118ed6f76e94867ed454cf798a6f312ce2c4cdc1e035a25d810d754e4cb2e4d866acb4219490f3585de60 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.23.9": version: 7.23.9 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.23.9" @@ -1301,6 +1612,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typescript@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-typescript@npm:7.25.9" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + "@babel/helper-create-class-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + "@babel/plugin-syntax-typescript": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/91e2ec805f89a813e0bf9cf42dffb767f798429e983af3e2f919885a2826b10f29223dd8b40ccc569eb61858d3273620e82e14431603a893e4a7f9b4c1a3a3cf + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-escapes@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.23.3" @@ -1451,6 +1777,21 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:^7.20.2": + version: 7.26.0 + resolution: "@babel/preset-typescript@npm:7.26.0" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-validator-option": "npm:^7.25.9" + "@babel/plugin-syntax-jsx": "npm:^7.25.9" + "@babel/plugin-transform-modules-commonjs": "npm:^7.25.9" + "@babel/plugin-transform-typescript": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/81a60826160163a3daae017709f42147744757b725b50c9024ef3ee5a402ee45fd2e93eaecdaaa22c81be91f7940916249cfb7711366431cfcacc69c95878c03 + languageName: node + linkType: hard + "@babel/regjsgen@npm:^0.8.0": version: 0.8.0 resolution: "@babel/regjsgen@npm:0.8.0" @@ -1467,7 +1808,18 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.24.0, @babel/template@npm:^7.25.0, @babel/template@npm:^7.3.3": +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.24.0, @babel/template@npm:^7.3.3": + version: 7.24.0 + resolution: "@babel/template@npm:7.24.0" + dependencies: + "@babel/code-frame": "npm:^7.23.5" + "@babel/parser": "npm:^7.24.0" + "@babel/types": "npm:^7.24.0" + checksum: 10/8c538338c7de8fac8ada691a5a812bdcbd60bd4a4eb5adae2cc9ee19773e8fb1a724312a00af9e1ce49056ffd3c3475e7287b5668cf6360bfb3f8ac827a06ffe + languageName: node + linkType: hard + +"@babel/template@npm:^7.25.0": version: 7.25.0 resolution: "@babel/template@npm:7.25.0" dependencies: @@ -1478,7 +1830,36 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.24.5, @babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.7.2": +"@babel/template@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/template@npm:7.25.9" + dependencies: + "@babel/code-frame": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e861180881507210150c1335ad94aff80fd9e9be6202e1efa752059c93224e2d5310186ddcdd4c0f0b0fc658ce48cb47823f15142b5c00c8456dde54f5de80b2 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.24.5, @babel/traverse@npm:^7.7.2": + version: 7.24.5 + resolution: "@babel/traverse@npm:7.24.5" + dependencies: + "@babel/code-frame": "npm:^7.24.2" + "@babel/generator": "npm:^7.24.5" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-hoist-variables": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.24.5" + "@babel/parser": "npm:^7.24.5" + "@babel/types": "npm:^7.24.5" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/e237de56e0c30795293fdb6f2cb09a75e6230836e3dc67dc4fa21781eb4d5842996bf3af95bc57ac5c7e6e97d06446f14732d0952eb57d5d9643de7c4f95bee6 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.24.7": version: 7.25.6 resolution: "@babel/traverse@npm:7.25.6" dependencies: @@ -1493,6 +1874,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/traverse@npm:7.25.9" + dependencies: + "@babel/code-frame": "npm:^7.25.9" + "@babel/generator": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" + "@babel/template": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/7431614d76d4a053e429208db82f2846a415833f3d9eb2e11ef72eeb3c64dfd71f4a4d983de1a4a047b36165a1f5a64de8ca2a417534cc472005c740ffcb9c6a + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.5, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.6, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.25.6 resolution: "@babel/types@npm:7.25.6" @@ -1504,6 +1900,27 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.24.0, @babel/types@npm:^7.8.3": + version: 7.24.5 + resolution: "@babel/types@npm:7.24.5" + dependencies: + "@babel/helper-string-parser": "npm:^7.24.1" + "@babel/helper-validator-identifier": "npm:^7.24.5" + to-fast-properties: "npm:^2.0.0" + checksum: 10/259e7512476ae64830e73f2addf143159232bcbf0eba6a6a27cab25a960cd353a11c826eb54185fdf7d8d9865922cbcd6522149e9ec55b967131193f9c9111a1 + languageName: node + linkType: hard + +"@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -1511,6 +1928,13 @@ __metadata: languageName: node linkType: hard +"@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0": + version: 1.6.0 + resolution: "@colors/colors@npm:1.6.0" + checksum: 10/66d00284a3a9a21e5e853b256942e17edbb295f4bd7b9aa7ef06bbb603568d5173eb41b0f64c1e51748bc29d382a23a67d99956e57e7431c64e47e74324182d9 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -1520,6 +1944,17 @@ __metadata: languageName: node linkType: hard +"@dabh/diagnostics@npm:^2.0.2": + version: 2.0.3 + resolution: "@dabh/diagnostics@npm:2.0.3" + dependencies: + colorspace: "npm:1.1.x" + enabled: "npm:2.0.x" + kuler: "npm:^2.0.0" + checksum: 10/14e449a7f42f063f959b472f6ce02d16457a756e852a1910aaa831b63fc21d86f6c32b2a1aa98a4835b856548c926643b51062d241fb6e9b2b7117996053e6b9 + languageName: node + linkType: hard + "@develar/schema-utils@npm:~2.6.5": version: 2.6.5 resolution: "@develar/schema-utils@npm:2.6.5" @@ -2081,14 +2516,14 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0": +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.6.1": version: 4.11.0 resolution: "@eslint-community/regexpp@npm:4.11.0" checksum: 10/f053f371c281ba173fe6ee16dbc4fe544c84870d58035ccca08dba7f6ce1830d895ce3237a0db89ba37616524775dca82f1c502066b58e2d5712d7f87f5ba17c languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.12.1": +"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.4.0": version: 4.12.1 resolution: "@eslint-community/regexpp@npm:4.12.1" checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc @@ -2115,6 +2550,23 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" + dependencies: + ajv: "npm:^6.12.4" + debug: "npm:^4.3.2" + espree: "npm:^9.6.0" + globals: "npm:^13.19.0" + ignore: "npm:^5.2.0" + import-fresh: "npm:^3.2.1" + js-yaml: "npm:^4.1.0" + minimatch: "npm:^3.1.2" + strip-json-comments: "npm:^3.1.1" + checksum: 10/7a3b14f4b40fc1a22624c3f84d9f467a3d9ea1ca6e9a372116cb92507e485260359465b58e25bcb6c9981b155416b98c9973ad9b796053fd7b3f776a6946bce8 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^3.2.0": version: 3.2.0 resolution: "@eslint/eslintrc@npm:3.2.0" @@ -2132,6 +2584,13 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:8.57.1": + version: 8.57.1 + resolution: "@eslint/js@npm:8.57.1" + checksum: 10/7562b21be10c2adbfa4aa5bb2eccec2cb9ac649a3569560742202c8d1cb6c931ce634937a2f0f551e078403a1c1285d6c2c0aa345dafc986149665cd69fe8b59 + languageName: node + linkType: hard + "@eslint/js@npm:9.17.0": version: 9.17.0 resolution: "@eslint/js@npm:9.17.0" @@ -2260,6 +2719,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.13.0": + version: 0.13.0 + resolution: "@humanwhocodes/config-array@npm:0.13.0" + dependencies: + "@humanwhocodes/object-schema": "npm:^2.0.3" + debug: "npm:^4.3.1" + minimatch: "npm:^3.0.5" + checksum: 10/524df31e61a85392a2433bf5d03164e03da26c03d009f27852e7dcfdafbc4a23f17f021dacf88e0a7a9fe04ca032017945d19b57a16e2676d9114c22a53a9d11 + languageName: node + linkType: hard + "@humanwhocodes/module-importer@npm:^1.0.1": version: 1.0.1 resolution: "@humanwhocodes/module-importer@npm:1.0.1" @@ -2267,6 +2737,13 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/object-schema@npm:^2.0.3": + version: 2.0.3 + resolution: "@humanwhocodes/object-schema@npm:2.0.3" + checksum: 10/05bb99ed06c16408a45a833f03a732f59bf6184795d4efadd33238ff8699190a8c871ad1121241bb6501589a9598dc83bf25b99dcbcf41e155cdf36e35e937a3 + languageName: node + linkType: hard + "@humanwhocodes/retry@npm:^0.3.0": version: 0.3.1 resolution: "@humanwhocodes/retry@npm:0.3.1" @@ -2345,7 +2822,7 @@ __metadata: languageName: node linkType: hard -"@istanbuljs/schema@npm:^0.1.2": +"@istanbuljs/schema@npm:^0.1.2, @istanbuljs/schema@npm:^0.1.3": version: 0.1.3 resolution: "@istanbuljs/schema@npm:0.1.3" checksum: 10/a9b1e49acdf5efc2f5b2359f2df7f90c5c725f2656f16099e8b2cd3a000619ecca9fc48cf693ba789cf0fd989f6e0df6a22bc05574be4223ecdbb7997d04384b @@ -2380,6 +2857,20 @@ __metadata: languageName: node linkType: hard +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10/4a80c750e8a31f344233cb9951dee9b77bf6b89377cb131f8b3cde07ff218f504370133a5963f6a786af4d2ce7f85642db206ff7a15f99fe58df4c38ac04899e + languageName: node + linkType: hard + "@jest/core@npm:^27.5.1": version: 27.5.1 resolution: "@jest/core@npm:27.5.1" @@ -2421,27 +2912,80 @@ __metadata: languageName: node linkType: hard -"@jest/create-cache-key-function@npm:^29.7.0": +"@jest/core@npm:^29.7.0": version: 29.7.0 - resolution: "@jest/create-cache-key-function@npm:29.7.0" + resolution: "@jest/core@npm:29.7.0" dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/reporters": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" "@jest/types": "npm:^29.6.3" - checksum: 10/061ef63b13ec8c8e5d08e4456f03b5cf8c7f9c1cab4fed8402e1479153cafce6eea80420e308ef62027abb7e29b825fcfa06551856bd021d98e92e381bf91723 - languageName: node - linkType: hard - -"@jest/environment@npm:^27.5.1": - version: 27.5.1 - resolution: "@jest/environment@npm:27.5.1" - dependencies: - "@jest/fake-timers": "npm:^27.5.1" - "@jest/types": "npm:^27.5.1" "@types/node": "npm:*" - jest-mock: "npm:^27.5.1" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-changed-files: "npm:^29.7.0" + jest-config: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-resolve-dependencies: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-ansi: "npm:^6.0.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10/ab6ac2e562d083faac7d8152ec1cc4eccc80f62e9579b69ed40aedf7211a6b2d57024a6cd53c4e35fd051c39a236e86257d1d99ebdb122291969a0a04563b51e + languageName: node + linkType: hard + +"@jest/create-cache-key-function@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/create-cache-key-function@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + checksum: 10/061ef63b13ec8c8e5d08e4456f03b5cf8c7f9c1cab4fed8402e1479153cafce6eea80420e308ef62027abb7e29b825fcfa06551856bd021d98e92e381bf91723 + languageName: node + linkType: hard + +"@jest/environment@npm:^27.5.1": + version: 27.5.1 + resolution: "@jest/environment@npm:27.5.1" + dependencies: + "@jest/fake-timers": "npm:^27.5.1" + "@jest/types": "npm:^27.5.1" + "@types/node": "npm:*" + jest-mock: "npm:^27.5.1" checksum: 10/74a2a4427f82b096c4f7223c56a27f64487ee4639b017129f31e99ebb2e9a614eb365ec77c3701d6eedc1c8d711ad2dd4b31d6dfad72cbb6d73a4f1fdc4a86cb languageName: node linkType: hard +"@jest/environment@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/environment@npm:29.7.0" + dependencies: + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + checksum: 10/90b5844a9a9d8097f2cf107b1b5e57007c552f64315da8c1f51217eeb0a9664889d3f145cdf8acf23a84f4d8309a6675e27d5b059659a004db0ea9546d1c81a8 + languageName: node + linkType: hard + "@jest/expect-utils@npm:^29.5.0": version: 29.5.0 resolution: "@jest/expect-utils@npm:29.5.0" @@ -2451,6 +2995,25 @@ __metadata: languageName: node linkType: hard +"@jest/expect-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect-utils@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + checksum: 10/ef8d379778ef574a17bde2801a6f4469f8022a46a5f9e385191dc73bb1fc318996beaed4513fbd7055c2847227a1bed2469977821866534593a6e52a281499ee + languageName: node + linkType: hard + +"@jest/expect@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect@npm:29.7.0" + dependencies: + expect: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + checksum: 10/fea6c3317a8da5c840429d90bfe49d928e89c9e89fceee2149b93a11b7e9c73d2f6e4d7cdf647163da938fc4e2169e4490be6bae64952902bc7a701033fd4880 + languageName: node + linkType: hard + "@jest/fake-timers@npm:^27.5.1": version: 27.5.1 resolution: "@jest/fake-timers@npm:27.5.1" @@ -2465,6 +3028,20 @@ __metadata: languageName: node linkType: hard +"@jest/fake-timers@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/fake-timers@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@sinonjs/fake-timers": "npm:^10.0.2" + "@types/node": "npm:*" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10/9b394e04ffc46f91725ecfdff34c4e043eb7a16e1d78964094c9db3fde0b1c8803e45943a980e8c740d0a3d45661906de1416ca5891a538b0660481a3a828c27 + languageName: node + linkType: hard + "@jest/globals@npm:^27.5.1": version: 27.5.1 resolution: "@jest/globals@npm:27.5.1" @@ -2476,6 +3053,18 @@ __metadata: languageName: node linkType: hard +"@jest/globals@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/globals@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + jest-mock: "npm:^29.7.0" + checksum: 10/97dbb9459135693ad3a422e65ca1c250f03d82b2a77f6207e7fa0edd2c9d2015fbe4346f3dc9ebff1678b9d8da74754d4d440b7837497f8927059c0642a22123 + languageName: node + linkType: hard + "@jest/reporters@npm:^27.5.1": version: 27.5.1 resolution: "@jest/reporters@npm:27.5.1" @@ -2514,6 +3103,43 @@ __metadata: languageName: node linkType: hard +"@jest/reporters@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/reporters@npm:29.7.0" + dependencies: + "@bcoe/v8-coverage": "npm:^0.2.3" + "@jest/console": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + collect-v8-coverage: "npm:^1.0.0" + exit: "npm:^0.1.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + istanbul-lib-coverage: "npm:^3.0.0" + istanbul-lib-instrument: "npm:^6.0.0" + istanbul-lib-report: "npm:^3.0.0" + istanbul-lib-source-maps: "npm:^4.0.0" + istanbul-reports: "npm:^3.1.3" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + slash: "npm:^3.0.0" + string-length: "npm:^4.0.1" + strip-ansi: "npm:^6.0.0" + v8-to-istanbul: "npm:^9.0.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10/a17d1644b26dea14445cedd45567f4ba7834f980be2ef74447204e14238f121b50d8b858fde648083d2cd8f305f81ba434ba49e37a5f4237a6f2a61180cc73dc + languageName: node + linkType: hard + "@jest/schemas@npm:^29.6.3": version: 29.6.3 resolution: "@jest/schemas@npm:29.6.3" @@ -2534,6 +3160,17 @@ __metadata: languageName: node linkType: hard +"@jest/source-map@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/source-map@npm:29.6.3" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.18" + callsites: "npm:^3.0.0" + graceful-fs: "npm:^4.2.9" + checksum: 10/bcc5a8697d471396c0003b0bfa09722c3cd879ad697eb9c431e6164e2ea7008238a01a07193dfe3cbb48b1d258eb7251f6efcea36f64e1ebc464ea3c03ae2deb + languageName: node + linkType: hard + "@jest/test-result@npm:^27.5.1": version: 27.5.1 resolution: "@jest/test-result@npm:27.5.1" @@ -2558,6 +3195,18 @@ __metadata: languageName: node linkType: hard +"@jest/test-result@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-result@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + collect-v8-coverage: "npm:^1.0.0" + checksum: 10/c073ab7dfe3c562bff2b8fee6cc724ccc20aa96bcd8ab48ccb2aa309b4c0c1923a9e703cea386bd6ae9b71133e92810475bb9c7c22328fc63f797ad3324ed189 + languageName: node + linkType: hard + "@jest/test-sequencer@npm:^27.5.1": version: 27.5.1 resolution: "@jest/test-sequencer@npm:27.5.1" @@ -2570,6 +3219,18 @@ __metadata: languageName: node linkType: hard +"@jest/test-sequencer@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-sequencer@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10/4420c26a0baa7035c5419b0892ff8ffe9a41b1583ec54a10db3037cd46a7e29dd3d7202f8aa9d376e9e53be5f8b1bc0d16e1de6880a6d319b033b01dc4c8f639 + languageName: node + linkType: hard + "@jest/transform@npm:^27.5.1": version: 27.5.1 resolution: "@jest/transform@npm:27.5.1" @@ -2616,6 +3277,29 @@ __metadata: languageName: node linkType: hard +"@jest/transform@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/transform@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + babel-plugin-istanbul: "npm:^6.1.1" + chalk: "npm:^4.0.0" + convert-source-map: "npm:^2.0.0" + fast-json-stable-stringify: "npm:^2.1.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pirates: "npm:^4.0.4" + slash: "npm:^3.0.0" + write-file-atomic: "npm:^4.0.2" + checksum: 10/30f42293545ab037d5799c81d3e12515790bb58513d37f788ce32d53326d0d72ebf5b40f989e6896739aa50a5f77be44686e510966370d58511d5ad2637c68c1 + languageName: node + linkType: hard + "@jest/types@npm:^27.5.1": version: 27.5.1 resolution: "@jest/types@npm:27.5.1" @@ -2702,7 +3386,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.15, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.9": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.15, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: @@ -2749,6 +3433,164 @@ __metadata: languageName: node linkType: hard +"@mapbox/node-pre-gyp@npm:^1.0.11": + version: 1.0.11 + resolution: "@mapbox/node-pre-gyp@npm:1.0.11" + dependencies: + detect-libc: "npm:^2.0.0" + https-proxy-agent: "npm:^5.0.0" + make-dir: "npm:^3.1.0" + node-fetch: "npm:^2.6.7" + nopt: "npm:^5.0.0" + npmlog: "npm:^5.0.1" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.11" + bin: + node-pre-gyp: bin/node-pre-gyp + checksum: 10/59529a2444e44fddb63057152452b00705aa58059079191126c79ac1388ae4565625afa84ed4dd1bf017d1111ab6e47907f7c5192e06d83c9496f2f3e708680a + languageName: node + linkType: hard + +"@ngrok/ngrok-android-arm-eabi@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-android-arm-eabi@npm:1.4.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@ngrok/ngrok-android-arm64@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-android-arm64@npm:1.4.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@ngrok/ngrok-darwin-arm64@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-darwin-arm64@npm:1.4.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@ngrok/ngrok-darwin-universal@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-darwin-universal@npm:1.4.1" + conditions: os=darwin + languageName: node + linkType: hard + +"@ngrok/ngrok-darwin-x64@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-darwin-x64@npm:1.4.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@ngrok/ngrok-freebsd-x64@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-freebsd-x64@npm:1.4.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@ngrok/ngrok-linux-arm-gnueabihf@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-linux-arm-gnueabihf@npm:1.4.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@ngrok/ngrok-linux-arm64-gnu@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-linux-arm64-gnu@npm:1.4.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@ngrok/ngrok-linux-arm64-musl@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-linux-arm64-musl@npm:1.4.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@ngrok/ngrok-linux-x64-gnu@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-linux-x64-gnu@npm:1.4.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@ngrok/ngrok-linux-x64-musl@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-linux-x64-musl@npm:1.4.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@ngrok/ngrok-win32-ia32-msvc@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-win32-ia32-msvc@npm:1.4.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@ngrok/ngrok-win32-x64-msvc@npm:1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok-win32-x64-msvc@npm:1.4.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@ngrok/ngrok@npm:^1.4.1": + version: 1.4.1 + resolution: "@ngrok/ngrok@npm:1.4.1" + dependencies: + "@ngrok/ngrok-android-arm-eabi": "npm:1.4.1" + "@ngrok/ngrok-android-arm64": "npm:1.4.1" + "@ngrok/ngrok-darwin-arm64": "npm:1.4.1" + "@ngrok/ngrok-darwin-universal": "npm:1.4.1" + "@ngrok/ngrok-darwin-x64": "npm:1.4.1" + "@ngrok/ngrok-freebsd-x64": "npm:1.4.1" + "@ngrok/ngrok-linux-arm-gnueabihf": "npm:1.4.1" + "@ngrok/ngrok-linux-arm64-gnu": "npm:1.4.1" + "@ngrok/ngrok-linux-arm64-musl": "npm:1.4.1" + "@ngrok/ngrok-linux-x64-gnu": "npm:1.4.1" + "@ngrok/ngrok-linux-x64-musl": "npm:1.4.1" + "@ngrok/ngrok-win32-ia32-msvc": "npm:1.4.1" + "@ngrok/ngrok-win32-x64-msvc": "npm:1.4.1" + dependenciesMeta: + "@ngrok/ngrok-android-arm-eabi": + optional: true + "@ngrok/ngrok-android-arm64": + optional: true + "@ngrok/ngrok-darwin-arm64": + optional: true + "@ngrok/ngrok-darwin-universal": + optional: true + "@ngrok/ngrok-darwin-x64": + optional: true + "@ngrok/ngrok-freebsd-x64": + optional: true + "@ngrok/ngrok-linux-arm-gnueabihf": + optional: true + "@ngrok/ngrok-linux-arm64-gnu": + optional: true + "@ngrok/ngrok-linux-arm64-musl": + optional: true + "@ngrok/ngrok-linux-x64-gnu": + optional: true + "@ngrok/ngrok-linux-x64-musl": + optional: true + "@ngrok/ngrok-win32-ia32-msvc": + optional: true + "@ngrok/ngrok-win32-x64-msvc": + optional: true + checksum: 10/c86956756af6eb9f2cc47aba19ac14f59a0d031defc52ee6845b17363b051213abd29c984f11501d88a482f343fbadb0e7bf855803068bceb96bf5fb16dfb2cb + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -2766,7 +3608,7 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3": +"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -4892,6 +5734,24 @@ __metadata: languageName: node linkType: hard +"@sinonjs/commons@npm:^3.0.0": + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" + dependencies: + type-detect: "npm:4.0.8" + checksum: 10/a0af217ba7044426c78df52c23cedede6daf377586f3ac58857c565769358ab1f44ebf95ba04bbe38814fba6e316ca6f02870a009328294fc2c555d0f85a7117 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^10.0.2": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": "npm:^3.0.0" + checksum: 10/78155c7bd866a85df85e22028e046b8d46cf3e840f72260954f5e3ed5bd97d66c595524305a6841ffb3f681a08f6e5cef572a2cce5442a8a232dc29fb409b83e + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^8.0.1": version: 8.1.0 resolution: "@sinonjs/fake-timers@npm:8.1.0" @@ -5400,6 +6260,15 @@ __metadata: languageName: node linkType: hard +"@types/bcrypt@npm:^5.0.2": + version: 5.0.2 + resolution: "@types/bcrypt@npm:5.0.2" + dependencies: + "@types/node": "npm:*" + checksum: 10/b1f97532ffe6079cb57a464f28b5b37a30bc9620f43469e1f27ab9c979c8a114be5b667e7b115a5556fd5be463b65968da9bb32573c6faf74fecf6e565d8974b + languageName: node + linkType: hard + "@types/better-sqlite3@npm:^7.6.12": version: 7.6.12 resolution: "@types/better-sqlite3@npm:7.6.12" @@ -5409,6 +6278,16 @@ __metadata: languageName: node linkType: hard +"@types/body-parser@npm:*": + version: 1.19.5 + resolution: "@types/body-parser@npm:1.19.5" + dependencies: + "@types/connect": "npm:*" + "@types/node": "npm:*" + checksum: 10/1e251118c4b2f61029cc43b0dc028495f2d1957fe8ee49a707fb940f86a9bd2f9754230805598278fe99958b49e9b7e66eec8ef6a50ab5c1f6b93e1ba2aaba82 + languageName: node + linkType: hard + "@types/cacheable-request@npm:^6.0.1": version: 6.0.3 resolution: "@types/cacheable-request@npm:6.0.3" @@ -5421,6 +6300,22 @@ __metadata: languageName: node linkType: hard +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 10/7eb1bc5342a9604facd57598a6c62621e244822442976c443efb84ff745246b10d06e8b309b6e80130026a396f19bf6793b7cecd7380169f369dac3bfc46fb99 + languageName: node + linkType: hard + +"@types/cookiejar@npm:^2.1.5": + version: 2.1.5 + resolution: "@types/cookiejar@npm:2.1.5" + checksum: 10/04d5990e87b6387532d15a87d9ec9b2eb783039291193863751dcfd7fc723a3b3aa30ce4c06b03975cba58632e933772f1ff031af23eaa3ac7f94e71afa6e073 + languageName: node + linkType: hard + "@types/copyfiles@npm:^2": version: 2.4.4 resolution: "@types/copyfiles@npm:2.4.4" @@ -5428,10 +6323,19 @@ __metadata: languageName: node linkType: hard -"@types/d3-array@npm:^3.0.3": - version: 3.0.4 - resolution: "@types/d3-array@npm:3.0.4" - checksum: 10/22eb61b9f93ec3f562041eb5c6b8b366f07fefe675a8b8c5287222b3d8f8e17ce1e94337f4ae0f09e9417d44c50f4b2c34f6ab58da5a571285afe378fbe207ee +"@types/cors@npm:^2.8.13": + version: 2.8.17 + resolution: "@types/cors@npm:2.8.17" + dependencies: + "@types/node": "npm:*" + checksum: 10/469bd85e29a35977099a3745c78e489916011169a664e97c4c3d6538143b0a16e4cc72b05b407dc008df3892ed7bf595f9b7c0f1f4680e169565ee9d64966bde + languageName: node + linkType: hard + +"@types/d3-array@npm:^3.0.3": + version: 3.0.4 + resolution: "@types/d3-array@npm:3.0.4" + checksum: 10/22eb61b9f93ec3f562041eb5c6b8b366f07fefe675a8b8c5287222b3d8f8e17ce1e94337f4ae0f09e9417d44c50f4b2c34f6ab58da5a571285afe378fbe207ee languageName: node linkType: hard @@ -5551,6 +6455,63 @@ __metadata: languageName: node linkType: hard +"@types/express-actuator@npm:^1.8.0": + version: 1.8.3 + resolution: "@types/express-actuator@npm:1.8.3" + dependencies: + "@types/express": "npm:*" + checksum: 10/0da489ce311c36dcf6ee1a03ec082742b181aa8ac95eb884fe09f5635ee5621b575100133b3589e7e8f8056da608010bd7232c0c21aeee484735f5b8fce73948 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^4.17.33": + version: 4.19.6 + resolution: "@types/express-serve-static-core@npm:4.19.6" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10/a2e00b6c5993f0dd63ada2239be81076fe0220314b9e9fde586e8946c9c09ce60f9a2dd0d74410ee2b5fd10af8c3e755a32bb3abf134533e2158142488995455 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^5.0.0": + version: 5.0.1 + resolution: "@types/express-serve-static-core@npm:5.0.1" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10/9bccbf4c927a877e4fe60f9664737ec6ac39d4d906dbb2c8d00f67849bb0968833573c48602b5e77d3e0129fd1bdbe0eae08e68485f028ebf8c557806caa3377 + languageName: node + linkType: hard + +"@types/express@npm:*": + version: 5.0.0 + resolution: "@types/express@npm:5.0.0" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^5.0.0" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 10/45b199ab669caa33e6badafeebf078e277ea95042309d325a04b1ec498f33d33fd5a4ae9c8e358342367b178fe454d7323c5dfc8002bf27070b210a2c6cc11f0 + languageName: node + linkType: hard + +"@types/express@npm:^4.17.17": + version: 4.17.21 + resolution: "@types/express@npm:4.17.21" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^4.17.33" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 10/7a6d26cf6f43d3151caf4fec66ea11c9d23166e4f3102edfe45a94170654a54ea08cf3103d26b3928d7ebcc24162c90488e33986b7e3a5f8941225edd5eb18c7 + languageName: node + linkType: hard + "@types/fs-extra@npm:9.0.13, @types/fs-extra@npm:^9.0.11": version: 9.0.13 resolution: "@types/fs-extra@npm:9.0.13" @@ -5595,6 +6556,13 @@ __metadata: languageName: node linkType: hard +"@types/http-errors@npm:*": + version: 2.0.4 + resolution: "@types/http-errors@npm:2.0.4" + checksum: 10/1f3d7c3b32c7524811a45690881736b3ef741bf9849ae03d32ad1ab7062608454b150a4e7f1351f83d26a418b2d65af9bdc06198f1c079d75578282884c4e8e3 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -5630,6 +6598,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:^29.2.3": + version: 29.5.14 + resolution: "@types/jest@npm:29.5.14" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 10/59ec7a9c4688aae8ee529316c43853468b6034f453d08a2e1064b281af9c81234cec986be796288f1bbb29efe943bc950e70c8fa8faae1e460d50e3cf9760f9b + languageName: node + linkType: hard + "@types/jlongster__sql.js@npm:@types/sql.js@latest": version: 1.4.4 resolution: "@types/sql.js@npm:1.4.4" @@ -5640,7 +6618,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -5704,6 +6682,20 @@ __metadata: languageName: node linkType: hard +"@types/methods@npm:^1.1.4": + version: 1.1.4 + resolution: "@types/methods@npm:1.1.4" + checksum: 10/ad2a7178486f2fd167750f3eb920ab032a947ff2e26f55c86670a6038632d790b46f52e5b6ead5823f1e53fc68028f1e9ddd15cfead7903e04517c88debd72b1 + languageName: node + linkType: hard + +"@types/mime@npm:^1": + version: 1.3.5 + resolution: "@types/mime@npm:1.3.5" + checksum: 10/e29a5f9c4776f5229d84e525b7cd7dd960b51c30a0fb9a028c0821790b82fca9f672dab56561e2acd9e8eed51d431bde52eafdfef30f643586c4162f1aecfc78 + languageName: node + linkType: hard + "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -5727,6 +6719,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^17.0.45": + version: 17.0.45 + resolution: "@types/node@npm:17.0.45" + checksum: 10/b45fff7270b5e81be19ef91a66b764a8b21473a97a8d211218a52e3426b79ad48f371819ab9153370756b33ba284e5c875463de4d2cf48a472e9098d7f09e8a2 + languageName: node + linkType: hard + "@types/parse-json@npm:^4.0.0": version: 4.0.2 resolution: "@types/parse-json@npm:4.0.2" @@ -5774,6 +6773,20 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:*": + version: 6.9.16 + resolution: "@types/qs@npm:6.9.16" + checksum: 10/2e8918150c12735630f7ee16b770c72949274938c30306025f68aaf977227f41fe0c698ed93db1099e04916d582ac5a1faf7e3c7061c8d885d9169f59a184b6c + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 10/95640233b689dfbd85b8c6ee268812a732cf36d5affead89e806fe30da9a430767af8ef2cd661024fd97e19d61f3dec75af2df5e80ec3bea000019ab7028629a + languageName: node + linkType: hard + "@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.2.1": version: 18.2.1 resolution: "@types/react-dom@npm:18.2.1" @@ -5842,6 +6855,34 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.3.12": + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 + languageName: node + linkType: hard + +"@types/send@npm:*": + version: 0.17.4 + resolution: "@types/send@npm:0.17.4" + dependencies: + "@types/mime": "npm:^1" + "@types/node": "npm:*" + checksum: 10/28320a2aa1eb704f7d96a65272a07c0bf3ae7ed5509c2c96ea5e33238980f71deeed51d3631927a77d5250e4091b3e66bce53b42d770873282c6a20bb8b0280d + languageName: node + linkType: hard + +"@types/serve-static@npm:*": + version: 1.15.7 + resolution: "@types/serve-static@npm:1.15.7" + dependencies: + "@types/http-errors": "npm:*" + "@types/node": "npm:*" + "@types/send": "npm:*" + checksum: 10/c5a7171d5647f9fbd096ed1a26105759f3153ccf683824d99fee4c7eb9cde2953509621c56a070dd9fb1159e799e86d300cbe4e42245ebc5b0c1767e8ca94a67 + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.1 resolution: "@types/stack-utils@npm:2.0.1" @@ -5849,6 +6890,27 @@ __metadata: languageName: node linkType: hard +"@types/superagent@npm:*": + version: 8.1.9 + resolution: "@types/superagent@npm:8.1.9" + dependencies: + "@types/cookiejar": "npm:^2.1.5" + "@types/methods": "npm:^1.1.4" + "@types/node": "npm:*" + form-data: "npm:^4.0.0" + checksum: 10/6d9687b0bc3d693b900ef76000b02437a70879c3219b28606879c086d786bb1e48429813e72e32dd0aafc94c053a78a2aa8be67c45bc8e6b968ca62d6d5cc554 + languageName: node + linkType: hard + +"@types/supertest@npm:^2.0.12": + version: 2.0.16 + resolution: "@types/supertest@npm:2.0.16" + dependencies: + "@types/superagent": "npm:*" + checksum: 10/2fc998ea698e0467cdbe3bea0ebce2027ea3a45a13e51a6cecb0435f44b486faecf99c34d8702d2d7fe033e6e09fdd2b374af52ecc8d0c69a1deec66b8c0dd52 + languageName: node + linkType: hard + "@types/symlink-or-copy@npm:^1.2.0": version: 1.2.2 resolution: "@types/symlink-or-copy@npm:1.2.2" @@ -5856,6 +6918,13 @@ __metadata: languageName: node linkType: hard +"@types/triple-beam@npm:^1.3.2": + version: 1.3.5 + resolution: "@types/triple-beam@npm:1.3.5" + checksum: 10/519b6a1b30d4571965c9706ad5400a200b94e4050feca3e7856e3ea7ac00ec9903e32e9a10e2762d0f7e472d5d03e5f4b29c16c0bd8c1f77c8876c683b2231f1 + languageName: node + linkType: hard + "@types/trusted-types@npm:^2.0.2": version: 2.0.7 resolution: "@types/trusted-types@npm:2.0.7" @@ -5884,6 +6953,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^9.0.0": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: 10/b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 + languageName: node + linkType: hard + "@types/uuid@npm:^9.0.2": version: 9.0.2 resolution: "@types/uuid@npm:9.0.2" @@ -5975,6 +7051,30 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/eslint-plugin@npm:^5.51.0": + version: 5.62.0 + resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:5.62.0" + "@typescript-eslint/type-utils": "npm:5.62.0" + "@typescript-eslint/utils": "npm:5.62.0" + debug: "npm:^4.3.4" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.0" + natural-compare-lite: "npm:^1.4.0" + semver: "npm:^7.3.7" + tsutils: "npm:^3.21.0" + peerDependencies: + "@typescript-eslint/parser": ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/9cc8319c6fd8a21938f5b69476974a7e778c283a55ef9fad183c850995b9adcb0087d57cea7b2ac6b9449570eee983aad39491d14cdd2e52d6b4b0485e7b2482 + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:8.18.1, @typescript-eslint/parser@npm:^8.18.1": version: 8.18.1 resolution: "@typescript-eslint/parser@npm:8.18.1" @@ -5991,6 +7091,33 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^5.51.0": + version: 5.62.0 + resolution: "@typescript-eslint/parser@npm:5.62.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:5.62.0" + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/typescript-estree": "npm:5.62.0" + debug: "npm:^4.3.4" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b6ca629d8f4e6283ff124501731cc886703eb4ce2c7d38b3e4110322ea21452b9d9392faf25be6bd72f54b89de7ffc72a40d9b159083ac54345a3d04b4fa5394 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/scope-manager@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/visitor-keys": "npm:5.62.0" + checksum: 10/e827770baa202223bc0387e2fd24f630690809e460435b7dc9af336c77322290a770d62bd5284260fa881c86074d6a9fd6c97b07382520b115f6786b8ed499da + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.18.1": version: 8.18.1 resolution: "@typescript-eslint/scope-manager@npm:8.18.1" @@ -6011,6 +7138,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/type-utils@npm:5.62.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:5.62.0" + "@typescript-eslint/utils": "npm:5.62.0" + debug: "npm:^4.3.4" + tsutils: "npm:^3.21.0" + peerDependencies: + eslint: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/f9a4398d6d2aae09e3e765eff04cf4ab364376a87868031ac5c6a64c9bbb555cb1a7f99b07b3d1017e7422725b5f0bbee537f13b82ab2d930f161c987b3dece0 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.18.1": version: 8.18.1 resolution: "@typescript-eslint/type-utils@npm:8.18.1" @@ -6026,6 +7170,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/types@npm:5.62.0" + checksum: 10/24e8443177be84823242d6729d56af2c4b47bfc664dd411a1d730506abf2150d6c31bdefbbc6d97c8f91043e3a50e0c698239dcb145b79bb6b0c34469aaf6c45 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:8.18.1": version: 8.18.1 resolution: "@typescript-eslint/types@npm:8.18.1" @@ -6040,6 +7191,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/visitor-keys": "npm:5.62.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + semver: "npm:^7.3.7" + tsutils: "npm:^3.21.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/06c975eb5f44b43bd19fadc2e1023c50cf87038fe4c0dd989d4331c67b3ff509b17fa60a3251896668ab4d7322bdc56162a9926971218d2e1a1874d2bef9a52e + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.18.1": version: 8.18.1 resolution: "@typescript-eslint/typescript-estree@npm:8.18.1" @@ -6076,6 +7245,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/utils@npm:5.62.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@types/json-schema": "npm:^7.0.9" + "@types/semver": "npm:^7.3.12" + "@typescript-eslint/scope-manager": "npm:5.62.0" + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/typescript-estree": "npm:5.62.0" + eslint-scope: "npm:^5.1.1" + semver: "npm:^7.3.7" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10/15ef13e43998a082b15f85db979f8d3ceb1f9ce4467b8016c267b1738d5e7cdb12aa90faf4b4e6dd6486c236cf9d33c463200465cf25ff997dbc0f12358550a1 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.18.1": version: 8.18.1 resolution: "@typescript-eslint/utils@npm:8.18.1" @@ -6106,6 +7293,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + eslint-visitor-keys: "npm:^3.3.0" + checksum: 10/dc613ab7569df9bbe0b2ca677635eb91839dfb2ca2c6fa47870a5da4f160db0b436f7ec0764362e756d4164e9445d49d5eb1ff0b87f4c058946ae9d8c92eb388 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.18.1": version: 8.18.1 resolution: "@typescript-eslint/visitor-keys@npm:8.18.1" @@ -6126,6 +7323,13 @@ __metadata: languageName: node linkType: hard +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 10/c6fe89a505e513a7592e1438280db1c075764793a2397877ff1351721fe8792a966a5359769e30242b3cd023f2efb9e63ca2ca88019d73b564488cc20e3eab12 + languageName: node + linkType: hard + "@use-gesture/core@npm:10.3.0": version: 10.3.0 resolution: "@use-gesture/core@npm:10.3.0" @@ -6430,13 +7634,22 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:^1.0.0": +"abbrev@npm:1, abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" checksum: 10/2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 languageName: node linkType: hard +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: 10/ed84af329f1828327798229578b4fe03a4dd2596ba304083ebd2252666bdc1d7647d66d0b18704477e1f8aa315f055944aa6e859afebd341f12d0a53c37b4b40 + languageName: node + linkType: hard + "absurd-sql@npm:0.0.54": version: 0.0.54 resolution: "absurd-sql@npm:0.0.54" @@ -6446,6 +7659,16 @@ __metadata: languageName: node linkType: hard +"accepts@npm:~1.3.8": + version: 1.3.8 + resolution: "accepts@npm:1.3.8" + dependencies: + mime-types: "npm:~2.1.34" + negotiator: "npm:0.6.3" + checksum: 10/67eaaa90e2917c58418e7a9b89392002d2b1ccd69bcca4799135d0c632f3b082f23f4ae4ddeedbced5aa59bcc7bdf4699c69ebed4593696c922462b7bc5744d6 + languageName: node + linkType: hard + "acorn-globals@npm:^6.0.0": version: 6.0.0 resolution: "acorn-globals@npm:6.0.0" @@ -6497,7 +7720,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.10.0, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.2": +"acorn@npm:^8.0.4, acorn@npm:^8.10.0, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.11.2 resolution: "acorn@npm:8.11.2" bin: @@ -6515,6 +7738,50 @@ __metadata: languageName: node linkType: hard +"actual-sync@npm:*, actual-sync@workspace:packages/sync-server": + version: 0.0.0-use.local + resolution: "actual-sync@workspace:packages/sync-server" + dependencies: + "@actual-app/crdt": "npm:*" + "@actual-app/web": "npm:*" + "@babel/preset-typescript": "npm:^7.20.2" + "@types/bcrypt": "npm:^5.0.2" + "@types/better-sqlite3": "npm:^7.6.12" + "@types/cors": "npm:^2.8.13" + "@types/express": "npm:^4.17.17" + "@types/express-actuator": "npm:^1.8.0" + "@types/jest": "npm:^29.2.3" + "@types/node": "npm:^17.0.45" + "@types/supertest": "npm:^2.0.12" + "@types/uuid": "npm:^9.0.0" + "@typescript-eslint/eslint-plugin": "npm:^5.51.0" + "@typescript-eslint/parser": "npm:^5.51.0" + bcrypt: "npm:^5.1.1" + better-sqlite3: "npm:^11.7.0" + body-parser: "npm:^1.20.3" + cors: "npm:^2.8.5" + date-fns: "npm:^2.30.0" + debug: "npm:^4.3.4" + eslint: "npm:^8.33.0" + eslint-plugin-prettier: "npm:^4.2.1" + express: "npm:4.20.0" + express-actuator: "npm:1.8.4" + express-rate-limit: "npm:^6.7.0" + express-response-size: "npm:^0.0.3" + express-winston: "npm:^4.2.0" + jest: "npm:^29.3.1" + jws: "npm:^4.0.0" + migrate: "npm:^2.0.1" + nordigen-node: "npm:^1.4.0" + openid-client: "npm:^5.4.2" + prettier: "npm:^2.8.3" + supertest: "npm:^6.3.1" + typescript: "npm:^4.9.5" + uuid: "npm:^9.0.0" + winston: "npm:^3.14.2" + languageName: unknown + linkType: soft + "actual@workspace:.": version: 0.0.0-use.local resolution: "actual@workspace:." @@ -6748,6 +8015,16 @@ __metadata: languageName: node linkType: hard +"are-we-there-yet@npm:^2.0.0": + version: 2.0.0 + resolution: "are-we-there-yet@npm:2.0.0" + dependencies: + delegates: "npm:^1.0.0" + readable-stream: "npm:^3.6.0" + checksum: 10/ea6f47d14fc33ae9cbea3e686eeca021d9d7b9db83a306010dd04ad5f2c8b7675291b127d3fcbfcbd8fec26e47b3324ad5b469a6cc3733a582f2fe4e12fc6756 + languageName: node + linkType: hard + "are-we-there-yet@npm:^3.0.0": version: 3.0.1 resolution: "are-we-there-yet@npm:3.0.1" @@ -6807,6 +8084,13 @@ __metadata: languageName: node linkType: hard +"array-flatten@npm:1.1.1": + version: 1.1.1 + resolution: "array-flatten@npm:1.1.1" + checksum: 10/e13c9d247241be82f8b4ec71d035ed7204baa82fae820d4db6948d30d3c4a9f2b3905eb2eec2b937d4aa3565200bd3a1c500480114cff649fa748747d2a50feb + languageName: node + linkType: hard + "array-includes@npm:^3.1.6, array-includes@npm:^3.1.8": version: 3.1.8 resolution: "array-includes@npm:3.1.8" @@ -6909,6 +8193,13 @@ __metadata: languageName: node linkType: hard +"asap@npm:^2.0.0": + version: 2.0.6 + resolution: "asap@npm:2.0.6" + checksum: 10/b244c0458c571945e4b3be0b14eb001bea5596f9868cc50cc711dc03d58a7e953517d3f0dad81ccde3ff37d1f074701fa76a6f07d41aaa992d7204a37b915dda + languageName: node + linkType: hard + "assert-plus@npm:^1.0.0": version: 1.0.0 resolution: "assert-plus@npm:1.0.0" @@ -7003,6 +8294,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.2.1": + version: 1.7.7 + resolution: "axios@npm:1.7.7" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10/7f875ea13b9298cd7b40fd09985209f7a38d38321f1118c701520939de2f113c4ba137832fe8e3f811f99a38e12c8225481011023209a77b0c0641270e20cde1 + languageName: node + linkType: hard + "axobject-query@npm:^4.1.0": version: 4.1.0 resolution: "axobject-query@npm:4.1.0" @@ -7035,6 +8337,23 @@ __metadata: languageName: node linkType: hard +"babel-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "babel-jest@npm:29.7.0" + dependencies: + "@jest/transform": "npm:^29.7.0" + "@types/babel__core": "npm:^7.1.14" + babel-plugin-istanbul: "npm:^6.1.1" + babel-preset-jest: "npm:^29.6.3" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + slash: "npm:^3.0.0" + peerDependencies: + "@babel/core": ^7.8.0 + checksum: 10/8a0953bd813b3a8926008f7351611055548869e9a53dd36d6e7e96679001f71e65fd7dbfe253265c3ba6a4e630dc7c845cf3e78b17d758ef1880313ce8fba258 + languageName: node + linkType: hard + "babel-plugin-istanbul@npm:^6.1.1": version: 6.1.1 resolution: "babel-plugin-istanbul@npm:6.1.1" @@ -7060,6 +8379,18 @@ __metadata: languageName: node linkType: hard +"babel-plugin-jest-hoist@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-plugin-jest-hoist@npm:29.6.3" + dependencies: + "@babel/template": "npm:^7.3.3" + "@babel/types": "npm:^7.3.3" + "@types/babel__core": "npm:^7.1.14" + "@types/babel__traverse": "npm:^7.0.6" + checksum: 10/9bfa86ec4170bd805ab8ca5001ae50d8afcb30554d236ba4a7ffc156c1a92452e220e4acbd98daefc12bf0216fccd092d0a2efed49e7e384ec59e0597a926d65 + languageName: node + linkType: hard + "babel-plugin-macros@npm:^3.1.0": version: 3.1.0 resolution: "babel-plugin-macros@npm:3.1.0" @@ -7141,6 +8472,18 @@ __metadata: languageName: node linkType: hard +"babel-preset-jest@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-preset-jest@npm:29.6.3" + dependencies: + babel-plugin-jest-hoist: "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/aa4ff2a8a728d9d698ed521e3461a109a1e66202b13d3494e41eea30729a5e7cc03b3a2d56c594423a135429c37bf63a9fa8b0b9ce275298be3095a88c69f6fb + languageName: node + linkType: hard + "bail@npm:^2.0.0": version: 2.0.2 resolution: "bail@npm:2.0.2" @@ -7176,6 +8519,16 @@ __metadata: languageName: node linkType: hard +"bcrypt@npm:^5.1.1": + version: 5.1.1 + resolution: "bcrypt@npm:5.1.1" + dependencies: + "@mapbox/node-pre-gyp": "npm:^1.0.11" + node-addon-api: "npm:^5.0.0" + checksum: 10/be6af3a93d90a0071c3b4412e8b82e2f319e26cb4e6cb14a1790cfe7c164792fa8add3ac9f30278a017d7d332ee8852601ce81a69737e9bfb9f10c878dd3d0dd + languageName: node + linkType: hard + "better-sqlite3@npm:^11.7.0": version: 11.7.0 resolution: "better-sqlite3@npm:11.7.0" @@ -7264,6 +8617,26 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:1.20.3, body-parser@npm:^1.20.3": + version: 1.20.3 + resolution: "body-parser@npm:1.20.3" + dependencies: + bytes: "npm:3.1.2" + content-type: "npm:~1.0.5" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.4.24" + on-finished: "npm:2.4.1" + qs: "npm:6.13.0" + raw-body: "npm:2.5.2" + type-is: "npm:~1.6.18" + unpipe: "npm:1.0.0" + checksum: 10/8723e3d7a672eb50854327453bed85ac48d045f4958e81e7d470c56bf111f835b97e5b73ae9f6393d0011cc9e252771f46fd281bbabc57d33d3986edf1e6aeca + languageName: node + linkType: hard + "boolbase@npm:^1.0.0": version: 1.0.0 resolution: "boolbase@npm:1.0.0" @@ -7376,6 +8749,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.24.0": + version: 4.24.2 + resolution: "browserslist@npm:4.24.2" + dependencies: + caniuse-lite: "npm:^1.0.30001669" + electron-to-chromium: "npm:^1.5.41" + node-releases: "npm:^2.0.18" + update-browserslist-db: "npm:^1.1.1" + bin: + browserslist: cli.js + checksum: 10/f8a9d78bbabe466c57ffd5c50a9e5582a5df9aa68f43078ca62a9f6d0d6c70ba72eca72d0a574dbf177cf55cdca85a46f7eb474917a47ae5398c66f8b76f7d1c + languageName: node + linkType: hard + "bser@npm:2.1.1": version: 2.1.1 resolution: "bser@npm:2.1.1" @@ -7409,6 +8796,13 @@ __metadata: languageName: node linkType: hard +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 10/80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab + languageName: node + linkType: hard + "buffer-equal@npm:^1.0.0": version: 1.0.1 resolution: "buffer-equal@npm:1.0.1" @@ -7491,6 +8885,13 @@ __metadata: languageName: node linkType: hard +"bytes@npm:3.1.2": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: 10/a10abf2ba70c784471d6b4f58778c0beeb2b5d405148e66affa91f23a9f13d07603d0a0354667310ae1d6dc141474ffd44e2a074be0f6e2254edb8fc21445388 + languageName: node + linkType: hard + "cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" @@ -7639,6 +9040,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001669": + version: 1.0.30001677 + resolution: "caniuse-lite@npm:1.0.30001677" + checksum: 10/e07439bdeade5ffdd974691f44f8549ae0730fcf510acaa32d0b657c10370cd5aad09eeca37248966205fb37fce5f464dbce73ce177b4a1fdc3a34adbcfd7192 + languageName: node + linkType: hard + "caw@npm:^2.0.0": version: 2.0.1 resolution: "caw@npm:2.0.1" @@ -7988,7 +9396,7 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:^1.9.0": +"color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": version: 1.9.3 resolution: "color-convert@npm:1.9.3" dependencies: @@ -8013,14 +9421,24 @@ __metadata: languageName: node linkType: hard -"color-name@npm:~1.1.4": +"color-name@npm:^1.0.0, color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" checksum: 10/b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 languageName: node linkType: hard -"color-support@npm:^1.1.3": +"color-string@npm:^1.6.0": + version: 1.9.1 + resolution: "color-string@npm:1.9.1" + dependencies: + color-name: "npm:^1.0.0" + simple-swizzle: "npm:^0.2.2" + checksum: 10/72aa0b81ee71b3f4fb1ac9cd839cdbd7a011a7d318ef58e6cb13b3708dca75c7e45029697260488709f1b1c7ac4e35489a87e528156c1e365917d1c4ccb9b9cd + languageName: node + linkType: hard + +"color-support@npm:^1.1.2, color-support@npm:^1.1.3": version: 1.1.3 resolution: "color-support@npm:1.1.3" bin: @@ -8029,10 +9447,20 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.14, colorette@npm:^2.0.20": - version: 2.0.20 - resolution: "colorette@npm:2.0.20" - checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f +"color@npm:^3.1.3": + version: 3.2.1 + resolution: "color@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.3" + color-string: "npm:^1.6.0" + checksum: 10/bf70438e0192f4f62f4bfbb303e7231289e8cc0d15ff6b6cbdb722d51f680049f38d4fdfc057a99cb641895cf5e350478c61d98586400b060043afc44285e7ae + languageName: node + linkType: hard + +"colorette@npm:^2.0.14, colorette@npm:^2.0.20": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f languageName: node linkType: hard @@ -8043,6 +9471,16 @@ __metadata: languageName: node linkType: hard +"colorspace@npm:1.1.x": + version: 1.1.4 + resolution: "colorspace@npm:1.1.4" + dependencies: + color: "npm:^3.1.3" + text-hex: "npm:1.0.x" + checksum: 10/bb3934ef3c417e961e6d03d7ca60ea6e175947029bfadfcdb65109b01881a1c0ecf9c2b0b59abcd0ee4a0d7c1eae93beed01b0e65848936472270a0b341ebce8 + languageName: node + linkType: hard + "combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" @@ -8066,7 +9504,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^2.20.0, commander@npm:^2.8.1": +"commander@npm:^2.20.0, commander@npm:^2.20.3, commander@npm:^2.8.1": version: 2.20.3 resolution: "commander@npm:2.20.3" checksum: 10/90c5b6898610cd075984c58c4f88418a4fb44af08c1b1415e9854c03171bec31b336b7f3e4cefe33de994b3f12b03c5e2d638da4316df83593b9e82554e7e95b @@ -8122,6 +9560,13 @@ __metadata: languageName: node linkType: hard +"component-emitter@npm:^1.3.0": + version: 1.3.1 + resolution: "component-emitter@npm:1.3.1" + checksum: 10/94550aa462c7bd5a61c1bc480e28554aa306066930152d1b1844a0dd3845d4e5db7e261ddec62ae184913b3e59b55a2ad84093b9d3596a8f17c341514d6c483d + languageName: node + linkType: hard + "compute-scroll-into-view@npm:^2.0.4": version: 2.0.4 resolution: "compute-scroll-into-view@npm:2.0.4" @@ -8156,14 +9601,14 @@ __metadata: languageName: node linkType: hard -"console-control-strings@npm:^1.1.0": +"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" checksum: 10/27b5fa302bc8e9ae9e98c03c66d76ca289ad0c61ce2fe20ab288d288bee875d217512d2edb2363fc83165e88f1c405180cf3f5413a46e51b4fe1a004840c6cdb languageName: node linkType: hard -"content-disposition@npm:^0.5.2": +"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.2": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -8172,6 +9617,13 @@ __metadata: languageName: node linkType: hard +"content-type@npm:~1.0.4, content-type@npm:~1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 10/585847d98dc7fb8035c02ae2cb76c7a9bd7b25f84c447e5ed55c45c2175e83617c8813871b4ee22f368126af6b2b167df655829007b21aa10302873ea9c62662 + languageName: node + linkType: hard + "convert-source-map@npm:^1.4.0, convert-source-map@npm:^1.5.0, convert-source-map@npm:^1.6.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" @@ -8186,6 +9638,27 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:1.0.6": + version: 1.0.6 + resolution: "cookie-signature@npm:1.0.6" + checksum: 10/f4e1b0a98a27a0e6e66fd7ea4e4e9d8e038f624058371bf4499cfcd8f3980be9a121486995202ba3fca74fbed93a407d6d54d43a43f96fd28d0bd7a06761591a + languageName: node + linkType: hard + +"cookie@npm:0.6.0": + version: 0.6.0 + resolution: "cookie@npm:0.6.0" + checksum: 10/c1f8f2ea7d443b9331680598b0ae4e6af18a618c37606d1bbdc75bec8361cce09fe93e727059a673f2ba24467131a9fb5a4eec76bb1b149c1b3e1ccb268dc583 + languageName: node + linkType: hard + +"cookiejar@npm:^2.1.4": + version: 2.1.4 + resolution: "cookiejar@npm:2.1.4" + checksum: 10/4a184f5a0591df8b07d22a43ea5d020eacb4572c383e853a33361a99710437eaa0971716c688684075bbf695b484f5872e9e3f562382e46858716cb7fc8ce3f4 + languageName: node + linkType: hard + "copyfiles@npm:^2.4.1": version: 2.4.1 resolution: "copyfiles@npm:2.4.1" @@ -8234,6 +9707,16 @@ __metadata: languageName: node linkType: hard +"cors@npm:^2.8.5": + version: 2.8.5 + resolution: "cors@npm:2.8.5" + dependencies: + object-assign: "npm:^4" + vary: "npm:^1" + checksum: 10/66e88e08edee7cbce9d92b4d28a2028c88772a4c73e02f143ed8ca76789f9b59444eed6b1c167139e76fa662998c151322720093ba229f9941365ada5a6fc2c6 + languageName: node + linkType: hard + "cosmiconfig@npm:^7.0.0": version: 7.1.0 resolution: "cosmiconfig@npm:7.1.0" @@ -8268,6 +9751,23 @@ __metadata: languageName: node linkType: hard +"create-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "create-jest@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + prompts: "npm:^2.0.1" + bin: + create-jest: bin/create-jest.js + checksum: 10/847b4764451672b4174be4d5c6d7d63442ec3aa5f3de52af924e4d996d87d7801c18e125504f25232fc75840f6625b3ac85860fac6ce799b5efae7bdcaf4a2b7 + languageName: node + linkType: hard + "create-require@npm:^1.1.0": version: 1.1.1 resolution: "create-require@npm:1.1.1" @@ -8311,7 +9811,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.3": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -8607,6 +10107,20 @@ __metadata: languageName: node linkType: hard +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: 10/5c149c91bf9ce2142c89f84eee4c585f0cb1f6faf2536b1af89873f862666a28529d1ccafc44750aa01384da2197c4f76f4e149a3cc0c1cb2c46f5cc45f2bcb5 + languageName: node + linkType: hard + +"dayjs@npm:^1.11.3": + version: 1.11.13 + resolution: "dayjs@npm:1.11.13" + checksum: 10/7374d63ab179b8d909a95e74790def25c8986e329ae989840bacb8b1888be116d20e1c4eee75a69ea0dfbae13172efc50ef85619d304ee7ca3c01d5878b704f5 + languageName: node + linkType: hard + "debounce@npm:^1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -8614,6 +10128,15 @@ __metadata: languageName: node linkType: hard +"debug@npm:2.6.9, debug@npm:^2.2.0": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: "npm:2.0.0" + checksum: 10/e07005f2b40e04f1bd14a3dd20520e9c4f25f60224cb006ce9d6781732c917964e9ec029fc7f1a151083cd929025ad5133814d4dc624a9aaf020effe4914ed14 + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.6": version: 4.3.6 resolution: "debug@npm:4.3.6" @@ -8626,15 +10149,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^2.2.0": - version: 2.6.9 - resolution: "debug@npm:2.6.9" - dependencies: - ms: "npm:2.0.0" - checksum: 10/e07005f2b40e04f1bd14a3dd20520e9c4f25f60224cb006ce9d6781732c917964e9ec029fc7f1a151083cd929025ad5133814d4dc624a9aaf020effe4914ed14 - languageName: node - linkType: hard - "debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" @@ -8767,6 +10281,18 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^1.0.0": + version: 1.5.3 + resolution: "dedent@npm:1.5.3" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 10/e5277f6268f288649503125b781a7b7a2c9b22d011139688c0b3619fe40121e600eb1f077c891938d4b2428bdb6326cc3c77a763e4b1cc681bd9666ab1bad2a1 + languageName: node + linkType: hard + "deep-eql@npm:^4.1.3": version: 4.1.3 resolution: "deep-eql@npm:4.1.3" @@ -8882,7 +10408,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:^2.0.0": +"depd@npm:2.0.0, depd@npm:^2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: 10/c0c8ff36079ce5ada64f46cc9d6fd47ebcf38241105b6e0c98f412e8ad91f084bcf906ff644cc3a4bd876ca27a62accb8b0fff72ea6ed1a414b89d8506f4a5ca @@ -8902,8 +10428,10 @@ __metadata: dependencies: "@electron/notarize": "npm:2.4.0" "@electron/rebuild": "npm:3.6.0" + "@ngrok/ngrok": "npm:^1.4.1" "@types/copyfiles": "npm:^2" "@types/fs-extra": "npm:^11" + actual-sync: "npm:*" better-sqlite3: "npm:^11.7.0" copyfiles: "npm:^2.4.1" cross-env: "npm:^7.0.3" @@ -8915,6 +10443,13 @@ __metadata: languageName: unknown linkType: soft +"destroy@npm:1.2.0": + version: 1.2.0 + resolution: "destroy@npm:1.2.0" + checksum: 10/0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 + languageName: node + linkType: hard + "detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1": version: 2.0.2 resolution: "detect-libc@npm:2.0.2" @@ -8936,6 +10471,16 @@ __metadata: languageName: node linkType: hard +"dezalgo@npm:^1.0.4": + version: 1.0.4 + resolution: "dezalgo@npm:1.0.4" + dependencies: + asap: "npm:^2.0.0" + wrappy: "npm:1" + checksum: 10/895389c6aead740d2ab5da4d3466d20fa30f738010a4d3f4dcccc9fc645ca31c9d10b7e1804ae489b1eb02c7986f9f1f34ba132d409b043082a86d9a4e745624 + languageName: node + linkType: hard + "diff-sequences@npm:^27.5.1": version: 27.5.1 resolution: "diff-sequences@npm:27.5.1" @@ -9039,6 +10584,15 @@ __metadata: languageName: node linkType: hard +"doctrine@npm:^3.0.0": + version: 3.0.0 + resolution: "doctrine@npm:3.0.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: 10/b4b28f1df5c563f7d876e7461254a4597b8cabe915abe94d7c5d1633fed263fcf9a85e8d3836591fc2d040108e822b0d32758e5ec1fe31c590dc7e08086e3e48 + languageName: node + linkType: hard + "dom-accessibility-api@npm:^0.5.9": version: 0.5.16 resolution: "dom-accessibility-api@npm:0.5.16" @@ -9128,6 +10682,20 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^10.0.0": + version: 10.0.0 + resolution: "dotenv@npm:10.0.0" + checksum: 10/55f701ae213e3afe3f4232fae5edfb6e0c49f061a363ff9f1c5a0c2bf3fb990a6e49aeada11b2a116efb5fdc3bc3f1ef55ab330be43033410b267f7c0809a9dc + languageName: node + linkType: hard + +"dotenv@npm:^16.0.0": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 10/55a3134601115194ae0f924e54473459ed0d9fc340ae610b676e248cca45aa7c680d86365318ea964e6da4e2ea80c4514c1adab5adb43d6867fb57ff068f95c8 + languageName: node + linkType: hard + "dotenv@npm:^9.0.2": version: 9.0.2 resolution: "dotenv@npm:9.0.2" @@ -9201,6 +10769,22 @@ __metadata: languageName: node linkType: hard +"ecdsa-sig-formatter@npm:1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10/878e1aab8a42773320bc04c6de420bee21aebd71810e40b1799880a8a1c4594bcd6adc3d4213a0fb8147d4c3f529d8f9a618d7f59ad5a9a41b142058aceda23f + languageName: node + linkType: hard + +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: 10/1b4cac778d64ce3b582a7e26b218afe07e207a0f9bfe13cc7395a6d307849cfe361e65033c3251e00c27dd060cab43014c2d6b2647676135e18b77d2d05b3f4f + languageName: node + linkType: hard + "ejs@npm:^3.1.6, ejs@npm:^3.1.8": version: 3.1.10 resolution: "ejs@npm:3.1.10" @@ -9256,6 +10840,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.5.41": + version: 1.5.50 + resolution: "electron-to-chromium@npm:1.5.50" + checksum: 10/635ca4b593e64697fbebc9fe7f557abcb030e5f6edcefb596ae3f8c9313221a754b513b70f2ba12595a9ee5733442b2b58db9eed7a2fa63e9f7539d581dd4ac0 + languageName: node + linkType: hard + "electron@npm:30.0.6": version: 30.0.6 resolution: "electron@npm:30.0.6" @@ -9304,6 +10895,27 @@ __metadata: languageName: node linkType: hard +"enabled@npm:2.0.x": + version: 2.0.0 + resolution: "enabled@npm:2.0.0" + checksum: 10/9d256d89f4e8a46ff988c6a79b22fa814b4ffd82826c4fdacd9b42e9b9465709d3b748866d0ab4d442dfc6002d81de7f7b384146ccd1681f6a7f868d2acca063 + languageName: node + linkType: hard + +"encodeurl@npm:~1.0.2": + version: 1.0.2 + resolution: "encodeurl@npm:1.0.2" + checksum: 10/e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c + languageName: node + linkType: hard + +"encodeurl@npm:~2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: 10/abf5cd51b78082cf8af7be6785813c33b6df2068ce5191a40ca8b1afe6a86f9230af9a9ce694a5ce4665955e5c1120871826df9c128a642e09c58d592e2807fe + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -9784,6 +11396,20 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.2.0": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10/9d7169e3965b2f9ae46971afa392f6e5a25545ea30f2e2dd99c9b0a95a3f52b5653681a84f5b2911a413ddad2d7a93d3514165072f349b5ffc59c75a899970d6 + languageName: node + linkType: hard + +"escape-html@npm:~1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 10/6213ca9ae00d0ab8bccb6d8d4e0a98e76237b2410302cf7df70aaa6591d509a2a37ce8998008cbecae8fc8ffaadf3fb0229535e6a145f3ce0b211d060decbb24 + languageName: node + linkType: hard + "escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -10018,6 +11644,21 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-prettier@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-plugin-prettier@npm:4.2.1" + dependencies: + prettier-linter-helpers: "npm:^1.0.0" + peerDependencies: + eslint: ">=7.28.0" + prettier: ">=2.0.0" + peerDependenciesMeta: + eslint-config-prettier: + optional: true + checksum: 10/d387f85dd1bfcb6bc6b794845fee6afb9ebb2375653de6bcde6e615892fb97f85121a7c012a4651b181fc09953bdf54c9bc70cab7ad297019d89ae87dd007e28 + languageName: node + linkType: hard + "eslint-plugin-react-hooks@npm:^5.1.0": version: 5.1.0 resolution: "eslint-plugin-react-hooks@npm:5.1.0" @@ -10062,7 +11703,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:5.1.1": +"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" dependencies: @@ -10072,6 +11713,16 @@ __metadata: languageName: node linkType: hard +"eslint-scope@npm:^7.2.2": + version: 7.2.2 + resolution: "eslint-scope@npm:7.2.2" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 10/5c660fb905d5883ad018a6fea2b49f3cb5b1cbf2cd4bd08e98646e9864f9bc2c74c0839bed2d292e90a4a328833accc197c8f0baed89cbe8d605d6f918465491 + languageName: node + linkType: hard + "eslint-scope@npm:^8.2.0": version: 8.2.0 resolution: "eslint-scope@npm:8.2.0" @@ -10116,7 +11767,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0": +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b @@ -10144,6 +11795,54 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.33.0": + version: 8.57.1 + resolution: "eslint@npm:8.57.1" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/regexpp": "npm:^4.6.1" + "@eslint/eslintrc": "npm:^2.1.4" + "@eslint/js": "npm:8.57.1" + "@humanwhocodes/config-array": "npm:^0.13.0" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@nodelib/fs.walk": "npm:^1.2.8" + "@ungap/structured-clone": "npm:^1.2.0" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.2" + debug: "npm:^4.3.2" + doctrine: "npm:^3.0.0" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^7.2.2" + eslint-visitor-keys: "npm:^3.4.3" + espree: "npm:^9.6.1" + esquery: "npm:^1.4.2" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^6.0.1" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + globals: "npm:^13.19.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + is-path-inside: "npm:^3.0.3" + js-yaml: "npm:^4.1.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + levn: "npm:^0.4.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + strip-ansi: "npm:^6.0.1" + text-table: "npm:^0.2.0" + bin: + eslint: bin/eslint.js + checksum: 10/5504fa24879afdd9f9929b2fbfc2ee9b9441a3d464efd9790fbda5f05738858530182029f13323add68d19fec749d3ab4a70320ded091ca4432b1e9cc4ed104c + languageName: node + linkType: hard + "eslint@npm:^9.17.0": version: 9.17.0 resolution: "eslint@npm:9.17.0" @@ -10204,6 +11903,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^9.6.0, espree@npm:^9.6.1": + version: 9.6.1 + resolution: "espree@npm:9.6.1" + dependencies: + acorn: "npm:^8.9.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10/255ab260f0d711a54096bdeda93adff0eadf02a6f9b92f02b323e83a2b7fc258797919437ad331efec3930475feb0142c5ecaaf3cdab4befebd336d47d3f3134 + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -10214,6 +11924,15 @@ __metadata: languageName: node linkType: hard +"esquery@npm:^1.4.2": + version: 1.5.0 + resolution: "esquery@npm:1.5.0" + dependencies: + estraverse: "npm:^5.1.0" + checksum: 10/e65fcdfc1e0ff5effbf50fb4f31ea20143ae5df92bb2e4953653d8d40aa4bc148e0d06117a592ce4ea53eeab1dafdfded7ea7e22a5be87e82d73757329a1b01d + languageName: node + linkType: hard + "esquery@npm:^1.5.0": version: 1.6.0 resolution: "esquery@npm:1.6.0" @@ -10276,6 +11995,20 @@ __metadata: languageName: node linkType: hard +"etag@npm:~1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 10/571aeb3dbe0f2bbd4e4fadbdb44f325fc75335cd5f6f6b6a091e6a06a9f25ed5392f0863c5442acb0646787446e816f13cbfc6edce5b07658541dff573cab1ff + languageName: node + linkType: hard + +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 10/49ff46c3a7facbad3decb31f597063e761785d7fdb3920d4989d7b08c97a61c2f51183e2f3a03130c9088df88d4b489b1b79ab632219901f184f85158508f4c8 + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.1": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -10290,7 +12023,7 @@ __metadata: languageName: node linkType: hard -"events@npm:^3.2.0": +"events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" checksum: 10/a3d47e285e28d324d7180f1e493961a2bbb4cad6412090e4dec114f4db1f5b560c7696ee8e758f55e23913ede856e3689cd3aa9ae13c56b5d8314cd3b3ddd1be @@ -10396,6 +12129,19 @@ __metadata: languageName: node linkType: hard +"expect@npm:^29.0.0, expect@npm:^29.7.0": + version: 29.7.0 + resolution: "expect@npm:29.7.0" + dependencies: + "@jest/expect-utils": "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10/63f97bc51f56a491950fb525f9ad94f1916e8a014947f8d8445d3847a665b5471b768522d659f5e865db20b6c2033d2ac10f35fcbd881a4d26407a4f6f18451a + languageName: node + linkType: hard + "expect@npm:^29.5.0": version: 29.5.0 resolution: "expect@npm:29.5.0" @@ -10416,6 +12162,85 @@ __metadata: languageName: node linkType: hard +"express-actuator@npm:1.8.4": + version: 1.8.4 + resolution: "express-actuator@npm:1.8.4" + dependencies: + dayjs: "npm:^1.11.3" + properties-reader: "npm:^2.2.0" + checksum: 10/5f55418bfd660e8781a777bf48c7eeda2d0a38fa11b2f4670555123380f522eed5bb884df297fb420904663bd2057834227c0eea68d6f852432ac3bc77842c09 + languageName: node + linkType: hard + +"express-rate-limit@npm:^6.7.0": + version: 6.11.2 + resolution: "express-rate-limit@npm:6.11.2" + peerDependencies: + express: ^4 || ^5 + checksum: 10/9b482cf91e030edcb88292831b2515208ddb9ec92330c54fb487c700fe8ac5000c7f5d2623ae4913b5a7fcce8e9ef65eb017e28edc96ace0ed111c16b996ccfc + languageName: node + linkType: hard + +"express-response-size@npm:^0.0.3": + version: 0.0.3 + resolution: "express-response-size@npm:0.0.3" + dependencies: + on-headers: "npm:1.0.1" + checksum: 10/6c97395d225e5aa98338569842ed84e5d861f19f6f1c535e70c97d65ffe7301826299607f491c824f240952b39b4b18c3269ed3652ecb8bd0abb93c7ece7048b + languageName: node + linkType: hard + +"express-winston@npm:^4.2.0": + version: 4.2.0 + resolution: "express-winston@npm:4.2.0" + dependencies: + chalk: "npm:^2.4.2" + lodash: "npm:^4.17.21" + peerDependencies: + winston: ">=3.x <4" + checksum: 10/3a4fb701d81b75815ccdf19f93585adb3af7ad61b4f67e435bb324486d9e3773e85e8761fb1e4c3833b0a493f363c792e8688eba018d1920fd3ee6d2505e5b3a + languageName: node + linkType: hard + +"express@npm:4.20.0": + version: 4.20.0 + resolution: "express@npm:4.20.0" + dependencies: + accepts: "npm:~1.3.8" + array-flatten: "npm:1.1.1" + body-parser: "npm:1.20.3" + content-disposition: "npm:0.5.4" + content-type: "npm:~1.0.4" + cookie: "npm:0.6.0" + cookie-signature: "npm:1.0.6" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + finalhandler: "npm:1.2.0" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + merge-descriptors: "npm:1.0.3" + methods: "npm:~1.1.2" + on-finished: "npm:2.4.1" + parseurl: "npm:~1.3.3" + path-to-regexp: "npm:0.1.10" + proxy-addr: "npm:~2.0.7" + qs: "npm:6.11.0" + range-parser: "npm:~1.2.1" + safe-buffer: "npm:5.2.1" + send: "npm:0.19.0" + serve-static: "npm:1.16.0" + setprototypeof: "npm:1.2.0" + statuses: "npm:2.0.1" + type-is: "npm:~1.6.18" + utils-merge: "npm:1.0.1" + vary: "npm:~1.1.2" + checksum: 10/4131f566cf8f6d1611475d5ff5d0dbc5c628ad8b525aa2aa2b3da9a23a041efcce09ede10b8a31315b0258ac4e53208a009fd7669ee1eb385936a0d54adb3cde + languageName: node + linkType: hard + "ext-list@npm:^2.0.0": version: 2.2.2 resolution: "ext-list@npm:2.2.2" @@ -10546,6 +12371,13 @@ __metadata: languageName: node linkType: hard +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 + languageName: node + linkType: hard + "fastest-levenshtein@npm:^1.0.12": version: 1.0.16 resolution: "fastest-levenshtein@npm:1.0.16" @@ -10580,6 +12412,13 @@ __metadata: languageName: node linkType: hard +"fecha@npm:^4.2.0": + version: 4.2.3 + resolution: "fecha@npm:4.2.3" + checksum: 10/534ce630c8f63c116292145607fc18c0f06bfa2fd74094357bf65daacc5d3f4f2b285bf8eb112c3bbf98c5caa6d386cced797f44b9b1b33da0c0a81020444826 + languageName: node + linkType: hard + "fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": version: 3.2.0 resolution: "fetch-blob@npm:3.2.0" @@ -10590,6 +12429,15 @@ __metadata: languageName: node linkType: hard +"file-entry-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "file-entry-cache@npm:6.0.1" + dependencies: + flat-cache: "npm:^3.0.4" + checksum: 10/099bb9d4ab332cb93c48b14807a6918a1da87c45dce91d4b61fd40e6505d56d0697da060cb901c729c90487067d93c9243f5da3dc9c41f0358483bfdebca736b + languageName: node + linkType: hard + "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -10663,6 +12511,21 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:1.2.0": + version: 1.2.0 + resolution: "finalhandler@npm:1.2.0" + dependencies: + debug: "npm:2.6.9" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + on-finished: "npm:2.4.1" + parseurl: "npm:~1.3.3" + statuses: "npm:2.0.1" + unpipe: "npm:~1.0.0" + checksum: 10/635718cb203c6d18e6b48dfbb6c54ccb08ea470e4f474ddcef38c47edcf3227feec316f886dd701235997d8af35240cae49856721ce18f539ad038665ebbf163 + languageName: node + linkType: hard + "find-root@npm:^1.1.0": version: 1.1.0 resolution: "find-root@npm:1.1.0" @@ -10690,6 +12553,16 @@ __metadata: languageName: node linkType: hard +"flat-cache@npm:^3.0.4": + version: 3.0.4 + resolution: "flat-cache@npm:3.0.4" + dependencies: + flatted: "npm:^3.1.0" + rimraf: "npm:^3.0.2" + checksum: 10/9fe5d0cb97c988e3b25242e71346965fae22757674db3fca14206850af2efa3ca3b04a3ba0eba8d5e20fd8a3be80a2e14b1c2917e70ffe1acb98a8c3327e4c9f + languageName: node + linkType: hard + "flat-cache@npm:^4.0.0": version: 4.0.1 resolution: "flat-cache@npm:4.0.1" @@ -10700,6 +12573,13 @@ __metadata: languageName: node linkType: hard +"flatted@npm:^3.1.0": + version: 3.2.7 + resolution: "flatted@npm:3.2.7" + checksum: 10/427633049d55bdb80201c68f7eb1cbd533e03eac541f97d3aecab8c5526f12a20ccecaeede08b57503e772c769e7f8680b37e8d482d1e5f8d7e2194687f9ea35 + languageName: node + linkType: hard + "flatted@npm:^3.2.9": version: 3.3.2 resolution: "flatted@npm:3.3.2" @@ -10707,6 +12587,13 @@ __metadata: languageName: node linkType: hard +"fn.name@npm:1.x.x": + version: 1.1.0 + resolution: "fn.name@npm:1.1.0" + checksum: 10/000198af190ae02f0138ac5fa4310da733224c628e0230c81e3fff7c4e094af7e0e8bb9f4357cabd21db601759d89f3445da744afbae20623cfa41edf3888397 + languageName: node + linkType: hard + "focus-visible@npm:^4.1.5": version: 4.1.5 resolution: "focus-visible@npm:4.1.5" @@ -10714,7 +12601,17 @@ __metadata: languageName: node linkType: hard -"for-each@npm:^0.3.3": +"follow-redirects@npm:^1.15.6": + version: 1.15.9 + resolution: "follow-redirects@npm:1.15.9" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/e3ab42d1097e90d28b913903841e6779eb969b62a64706a3eb983e894a5db000fbd89296f45f08885a0e54cd558ef62e81be1165da9be25a6c44920da10f424c + languageName: node + linkType: hard + +"for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" dependencies: @@ -10764,6 +12661,32 @@ __metadata: languageName: node linkType: hard +"formidable@npm:^2.1.2": + version: 2.1.2 + resolution: "formidable@npm:2.1.2" + dependencies: + dezalgo: "npm:^1.0.4" + hexoid: "npm:^1.0.0" + once: "npm:^1.4.0" + qs: "npm:^6.11.0" + checksum: 10/d385180e0461f65e6f7b70452859fe1c32aa97a290c2ca33f00cdc33145ef44fa68bbc9b93af2c3af73ae726e42c3477c6619c49f3c34b49934e9481275b7b4c + languageName: node + linkType: hard + +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: 10/29ba9fd347117144e97cbb8852baae5e8b2acb7d1b591ef85695ed96f5b933b1804a7fac4a15dd09ca7ac7d0cdc104410e8102aae2dd3faa570a797ba07adb81 + languageName: node + linkType: hard + +"fresh@npm:0.5.2": + version: 0.5.2 + resolution: "fresh@npm:0.5.2" + checksum: 10/64c88e489b5d08e2f29664eb3c79c705ff9a8eb15d3e597198ef76546d4ade295897a44abb0abd2700e7ef784b2e3cbf1161e4fbf16f59129193fd1030d16da1 + languageName: node + linkType: hard + "fs-constants@npm:^1.0.0": version: 1.0.0 resolution: "fs-constants@npm:1.0.0" @@ -10948,6 +12871,23 @@ __metadata: languageName: node linkType: hard +"gauge@npm:^3.0.0": + version: 3.0.2 + resolution: "gauge@npm:3.0.2" + dependencies: + aproba: "npm:^1.0.3 || ^2.0.0" + color-support: "npm:^1.1.2" + console-control-strings: "npm:^1.0.0" + has-unicode: "npm:^2.0.1" + object-assign: "npm:^4.1.1" + signal-exit: "npm:^3.0.0" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + wide-align: "npm:^1.1.2" + checksum: 10/46df086451672a5fecd58f7ec86da74542c795f8e00153fbef2884286ce0e86653c3eb23be2d0abb0c4a82b9b2a9dec3b09b6a1cf31c28085fa0376599a26589 + languageName: node + linkType: hard + "gauge@npm:^4.0.3": version: 4.0.4 resolution: "gauge@npm:4.0.4" @@ -11217,6 +13157,15 @@ __metadata: languageName: node linkType: hard +"globals@npm:^13.19.0": + version: 13.20.0 + resolution: "globals@npm:13.20.0" + dependencies: + type-fest: "npm:^0.20.2" + checksum: 10/9df85cde2f0dce6ac9b3a5e08bec109d2f3b38ddd055a83867e0672c55704866d53ce6a4265859fa630624baadd46f50ca38602a13607ad86be853a8c179d3e7 + languageName: node + linkType: hard + "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -11250,7 +13199,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.4": +"globby@npm:^11.0.4, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -11503,6 +13452,13 @@ __metadata: languageName: node linkType: hard +"hexoid@npm:^1.0.0": + version: 1.0.0 + resolution: "hexoid@npm:1.0.0" + checksum: 10/f2271b8b6b0e13fb5a1eccf740f53ce8bae689c80b9498b854c447f9dc94f75f44e0de064c0e4660ecdbfa8942bb2b69973fdcb080187b45bbb409a3c71f19d4 + languageName: node + linkType: hard + "hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -11572,6 +13528,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:2.0.0": + version: 2.0.0 + resolution: "http-errors@npm:2.0.0" + dependencies: + depd: "npm:2.0.0" + inherits: "npm:2.0.4" + setprototypeof: "npm:1.2.0" + statuses: "npm:2.0.1" + toidentifier: "npm:1.0.1" + checksum: 10/0e7f76ee8ff8a33e58a3281a469815b893c41357378f408be8f6d4aa7d1efafb0da064625518e7078381b6a92325949b119dc38fcb30bdbc4e3a35f78c44c439 + languageName: node + linkType: hard + "http-proxy-agent@npm:^4.0.1": version: 4.0.1 resolution: "http-proxy-agent@npm:4.0.1" @@ -11814,7 +13783,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3, inherits@npm:~2.0.4": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3, inherits@npm:~2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 @@ -11886,6 +13855,13 @@ __metadata: languageName: node linkType: hard +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: 10/864d0cced0c0832700e9621913a6429ccdc67f37c1bd78fb8c6789fff35c9d167cb329134acad2290497a53336813ab4798d2794fd675d5eb33b5fdf0982b9ca + languageName: node + linkType: hard + "is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1": version: 1.1.1 resolution: "is-arguments@npm:1.1.1" @@ -11913,6 +13889,13 @@ __metadata: languageName: node linkType: hard +"is-arrayish@npm:^0.3.1": + version: 0.3.2 + resolution: "is-arrayish@npm:0.3.2" + checksum: 10/81a78d518ebd8b834523e25d102684ee0f7e98637136d3bdc93fd09636350fa06f1d8ca997ea28143d4d13cb1b69c0824f082db0ac13e1ab3311c10ffea60ade + languageName: node + linkType: hard + "is-async-function@npm:^2.0.0": version: 2.0.0 resolution: "is-async-function@npm:2.0.0" @@ -12262,6 +14245,13 @@ __metadata: languageName: node linkType: hard +"is-path-inside@npm:^3.0.3": + version: 3.0.3 + resolution: "is-path-inside@npm:3.0.3" + checksum: 10/abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 + languageName: node + linkType: hard + "is-plain-obj@npm:^1.0.0, is-plain-obj@npm:^1.1.0": version: 1.1.0 resolution: "is-plain-obj@npm:1.1.0" @@ -12554,6 +14544,19 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-instrument@npm:^6.0.0": + version: 6.0.3 + resolution: "istanbul-lib-instrument@npm:6.0.3" + dependencies: + "@babel/core": "npm:^7.23.9" + "@babel/parser": "npm:^7.23.9" + "@istanbuljs/schema": "npm:^0.1.3" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^7.5.4" + checksum: 10/aa5271c0008dfa71b6ecc9ba1e801bf77b49dc05524e8c30d58aaf5b9505e0cd12f25f93165464d4266a518c5c75284ecb598fbd89fec081ae77d2c9d3327695 + languageName: node + linkType: hard + "istanbul-lib-report@npm:^3.0.0": version: 3.0.0 resolution: "istanbul-lib-report@npm:3.0.0" @@ -12648,6 +14651,17 @@ __metadata: languageName: node linkType: hard +"jest-changed-files@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-changed-files@npm:29.7.0" + dependencies: + execa: "npm:^5.0.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + checksum: 10/3d93742e56b1a73a145d55b66e96711fbf87ef89b96c2fab7cfdfba8ec06612591a982111ca2b712bb853dbc16831ec8b43585a2a96b83862d6767de59cbf83d + languageName: node + linkType: hard + "jest-circus@npm:^27.5.1": version: 27.5.1 resolution: "jest-circus@npm:27.5.1" @@ -12675,6 +14689,34 @@ __metadata: languageName: node linkType: hard +"jest-circus@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-circus@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + co: "npm:^4.6.0" + dedent: "npm:^1.0.0" + is-generator-fn: "npm:^2.0.0" + jest-each: "npm:^29.7.0" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + pure-rand: "npm:^6.0.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10/716a8e3f40572fd0213bcfc1da90274bf30d856e5133af58089a6ce45089b63f4d679bd44e6be9d320e8390483ebc3ae9921981993986d21639d9019b523123d + languageName: node + linkType: hard + "jest-cli@npm:^27.5.1": version: 27.5.1 resolution: "jest-cli@npm:27.5.1" @@ -12702,6 +14744,32 @@ __metadata: languageName: node linkType: hard +"jest-cli@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-cli@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + create-jest: "npm:^29.7.0" + exit: "npm:^0.1.2" + import-local: "npm:^3.0.2" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + yargs: "npm:^17.3.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10/6cc62b34d002c034203065a31e5e9a19e7c76d9e8ef447a6f70f759c0714cb212c6245f75e270ba458620f9c7b26063cd8cf6cd1f7e3afd659a7cc08add17307 + languageName: node + linkType: hard + "jest-config@npm:^27.5.1": version: 27.5.1 resolution: "jest-config@npm:27.5.1" @@ -12739,6 +14807,44 @@ __metadata: languageName: node linkType: hard +"jest-config@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-config@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/test-sequencer": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-jest: "npm:^29.7.0" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + deepmerge: "npm:^4.2.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-circus: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + parse-json: "npm:^5.2.0" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-json-comments: "npm:^3.1.1" + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: 10/6bdf570e9592e7d7dd5124fc0e21f5fe92bd15033513632431b211797e3ab57eaa312f83cc6481b3094b72324e369e876f163579d60016677c117ec4853cf02b + languageName: node + linkType: hard + "jest-diff@npm:^27.5.1": version: 27.5.1 resolution: "jest-diff@npm:27.5.1" @@ -12763,6 +14869,18 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + diff-sequences: "npm:^29.6.3" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10/6f3a7eb9cd9de5ea9e5aa94aed535631fa6f80221832952839b3cb59dd419b91c20b73887deb0b62230d06d02d6b6cf34ebb810b88d904bb4fe1e2e4f0905c98 + languageName: node + linkType: hard + "jest-docblock@npm:^27.5.1": version: 27.5.1 resolution: "jest-docblock@npm:27.5.1" @@ -12772,6 +14890,15 @@ __metadata: languageName: node linkType: hard +"jest-docblock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-docblock@npm:29.7.0" + dependencies: + detect-newline: "npm:^3.0.0" + checksum: 10/8d48818055bc96c9e4ec2e217a5a375623c0d0bfae8d22c26e011074940c202aa2534a3362294c81d981046885c05d304376afba9f2874143025981148f3e96d + languageName: node + linkType: hard + "jest-each@npm:^27.5.1": version: 27.5.1 resolution: "jest-each@npm:27.5.1" @@ -12785,6 +14912,19 @@ __metadata: languageName: node linkType: hard +"jest-each@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-each@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + pretty-format: "npm:^29.7.0" + checksum: 10/bd1a077654bdaa013b590deb5f7e7ade68f2e3289180a8c8f53bc8a49f3b40740c0ec2d3a3c1aee906f682775be2bebbac37491d80b634d15276b0aa0f2e3fda + languageName: node + linkType: hard + "jest-environment-jsdom@npm:^27.5.1": version: 27.5.1 resolution: "jest-environment-jsdom@npm:27.5.1" @@ -12814,6 +14954,20 @@ __metadata: languageName: node linkType: hard +"jest-environment-node@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-environment-node@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10/9cf7045adf2307cc93aed2f8488942e39388bff47ec1df149a997c6f714bfc66b2056768973770d3f8b1bf47396c19aa564877eb10ec978b952c6018ed1bd637 + languageName: node + linkType: hard + "jest-get-type@npm:^27.5.1": version: 27.5.1 resolution: "jest-get-type@npm:27.5.1" @@ -12828,6 +14982,13 @@ __metadata: languageName: node linkType: hard +"jest-get-type@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-get-type@npm:29.6.3" + checksum: 10/88ac9102d4679d768accae29f1e75f592b760b44277df288ad76ce5bf038c3f5ce3719dea8aa0f035dac30e9eb034b848ce716b9183ad7cc222d029f03e92205 + languageName: node + linkType: hard + "jest-haste-map@npm:^27.5.1": version: 27.5.1 resolution: "jest-haste-map@npm:27.5.1" @@ -12875,6 +15036,29 @@ __metadata: languageName: node linkType: hard +"jest-haste-map@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-haste-map@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/graceful-fs": "npm:^4.1.3" + "@types/node": "npm:*" + anymatch: "npm:^3.0.3" + fb-watchman: "npm:^2.0.0" + fsevents: "npm:^2.3.2" + graceful-fs: "npm:^4.2.9" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + walker: "npm:^1.0.8" + dependenciesMeta: + fsevents: + optional: true + checksum: 10/8531b42003581cb18a69a2774e68c456fb5a5c3280b1b9b77475af9e346b6a457250f9d756bfeeae2fe6cbc9ef28434c205edab9390ee970a919baddfa08bb85 + languageName: node + linkType: hard + "jest-jasmine2@npm:^27.5.1": version: 27.5.1 resolution: "jest-jasmine2@npm:27.5.1" @@ -12910,6 +15094,16 @@ __metadata: languageName: node linkType: hard +"jest-leak-detector@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-leak-detector@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10/e3950e3ddd71e1d0c22924c51a300a1c2db6cf69ec1e51f95ccf424bcc070f78664813bef7aed4b16b96dfbdeea53fe358f8aeaaea84346ae15c3735758f1605 + languageName: node + linkType: hard + "jest-matcher-utils@npm:^27.0.0, jest-matcher-utils@npm:^27.5.1": version: 27.5.1 resolution: "jest-matcher-utils@npm:27.5.1" @@ -12934,6 +15128,18 @@ __metadata: languageName: node linkType: hard +"jest-matcher-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-matcher-utils@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10/981904a494299cf1e3baed352f8a3bd8b50a8c13a662c509b6a53c31461f94ea3bfeffa9d5efcfeb248e384e318c87de7e3baa6af0f79674e987482aa189af40 + languageName: node + linkType: hard + "jest-message-util@npm:^27.5.1": version: 27.5.1 resolution: "jest-message-util@npm:27.5.1" @@ -12968,6 +15174,23 @@ __metadata: languageName: node linkType: hard +"jest-message-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-message-util@npm:29.7.0" + dependencies: + "@babel/code-frame": "npm:^7.12.13" + "@jest/types": "npm:^29.6.3" + "@types/stack-utils": "npm:^2.0.0" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10/31d53c6ed22095d86bab9d14c0fa70c4a92c749ea6ceece82cf30c22c9c0e26407acdfbdb0231435dc85a98d6d65ca0d9cbcd25cd1abb377fe945e843fb770b9 + languageName: node + linkType: hard + "jest-mock@npm:^27.5.1": version: 27.5.1 resolution: "jest-mock@npm:27.5.1" @@ -12978,6 +15201,17 @@ __metadata: languageName: node linkType: hard +"jest-mock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-mock@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + checksum: 10/ae51d1b4f898724be5e0e52b2268a68fcd876d9b20633c864a6dd6b1994cbc48d62402b0f40f3a1b669b30ebd648821f086c26c08ffde192ced951ff4670d51c + languageName: node + linkType: hard + "jest-pnp-resolver@npm:^1.2.2": version: 1.2.3 resolution: "jest-pnp-resolver@npm:1.2.3" @@ -13004,6 +15238,13 @@ __metadata: languageName: node linkType: hard +"jest-regex-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-regex-util@npm:29.6.3" + checksum: 10/0518beeb9bf1228261695e54f0feaad3606df26a19764bc19541e0fc6e2a3737191904607fb72f3f2ce85d9c16b28df79b7b1ec9443aa08c3ef0e9efda6f8f2a + languageName: node + linkType: hard + "jest-resolve-dependencies@npm:^27.5.1": version: 27.5.1 resolution: "jest-resolve-dependencies@npm:27.5.1" @@ -13015,6 +15256,16 @@ __metadata: languageName: node linkType: hard +"jest-resolve-dependencies@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve-dependencies@npm:29.7.0" + dependencies: + jest-regex-util: "npm:^29.6.3" + jest-snapshot: "npm:^29.7.0" + checksum: 10/1e206f94a660d81e977bcfb1baae6450cb4a81c92e06fad376cc5ea16b8e8c6ea78c383f39e95591a9eb7f925b6a1021086c38941aa7c1b8a6a813c2f6e93675 + languageName: node + linkType: hard + "jest-resolve@npm:^27.5.1": version: 27.5.1 resolution: "jest-resolve@npm:27.5.1" @@ -13033,6 +15284,23 @@ __metadata: languageName: node linkType: hard +"jest-resolve@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-pnp-resolver: "npm:^1.2.2" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + resolve: "npm:^1.20.0" + resolve.exports: "npm:^2.0.0" + slash: "npm:^3.0.0" + checksum: 10/faa466fd9bc69ea6c37a545a7c6e808e073c66f46ab7d3d8a6ef084f8708f201b85d5fe1799789578b8b47fa1de47b9ee47b414d1863bc117a49e032ba77b7c7 + languageName: node + linkType: hard + "jest-runner@npm:^27.5.1": version: 27.5.1 resolution: "jest-runner@npm:27.5.1" @@ -13062,6 +15330,35 @@ __metadata: languageName: node linkType: hard +"jest-runner@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runner@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/environment": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + graceful-fs: "npm:^4.2.9" + jest-docblock: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-leak-detector: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-resolve: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + source-map-support: "npm:0.5.13" + checksum: 10/9d8748a494bd90f5c82acea99be9e99f21358263ce6feae44d3f1b0cd90991b5df5d18d607e73c07be95861ee86d1cbab2a3fc6ca4b21805f07ac29d47c1da1e + languageName: node + linkType: hard + "jest-runtime@npm:^27.5.1": version: 27.5.1 resolution: "jest-runtime@npm:27.5.1" @@ -13092,6 +15389,36 @@ __metadata: languageName: node linkType: hard +"jest-runtime@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runtime@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/globals": "npm:^29.7.0" + "@jest/source-map": "npm:^29.6.3" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + cjs-module-lexer: "npm:^1.0.0" + collect-v8-coverage: "npm:^1.0.0" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-bom: "npm:^4.0.0" + checksum: 10/59eb58eb7e150e0834a2d0c0d94f2a0b963ae7182cfa6c63f2b49b9c6ef794e5193ef1634e01db41420c36a94cefc512cdd67a055cd3e6fa2f41eaf0f82f5a20 + languageName: node + linkType: hard + "jest-serializer@npm:^27.5.1": version: 27.5.1 resolution: "jest-serializer@npm:27.5.1" @@ -13163,6 +15490,34 @@ __metadata: languageName: node linkType: hard +"jest-snapshot@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-snapshot@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@babel/generator": "npm:^7.7.2" + "@babel/plugin-syntax-jsx": "npm:^7.7.2" + "@babel/plugin-syntax-typescript": "npm:^7.7.2" + "@babel/types": "npm:^7.3.3" + "@jest/expect-utils": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + chalk: "npm:^4.0.0" + expect: "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + natural-compare: "npm:^1.4.0" + pretty-format: "npm:^29.7.0" + semver: "npm:^7.5.3" + checksum: 10/cb19a3948256de5f922d52f251821f99657339969bf86843bd26cf3332eae94883e8260e3d2fba46129a27c3971c1aa522490e460e16c7fad516e82d10bbf9f8 + languageName: node + linkType: hard + "jest-util@npm:^27.5.1": version: 27.5.1 resolution: "jest-util@npm:27.5.1" @@ -13191,6 +15546,20 @@ __metadata: languageName: node linkType: hard +"jest-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-util@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + graceful-fs: "npm:^4.2.9" + picomatch: "npm:^2.2.3" + checksum: 10/30d58af6967e7d42bd903ccc098f3b4d3859ed46238fbc88d4add6a3f10bea00c226b93660285f058bc7a65f6f9529cf4eb80f8d4707f79f9e3a23686b4ab8f3 + languageName: node + linkType: hard + "jest-validate@npm:^27.5.1": version: 27.5.1 resolution: "jest-validate@npm:27.5.1" @@ -13205,6 +15574,20 @@ __metadata: languageName: node linkType: hard +"jest-validate@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-validate@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + camelcase: "npm:^6.2.0" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + leven: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + checksum: 10/8ee1163666d8eaa16d90a989edba2b4a3c8ab0ffaa95ad91b08ca42b015bfb70e164b247a5b17f9de32d096987cada63ed8491ab82761bfb9a28bc34b27ae161 + languageName: node + linkType: hard + "jest-watch-typeahead@npm:^2.2.2": version: 2.2.2 resolution: "jest-watch-typeahead@npm:2.2.2" @@ -13253,6 +15636,22 @@ __metadata: languageName: node linkType: hard +"jest-watcher@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-watcher@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + jest-util: "npm:^29.7.0" + string-length: "npm:^4.0.1" + checksum: 10/4f616e0345676631a7034b1d94971aaa719f0cd4a6041be2aa299be437ea047afd4fe05c48873b7963f5687a2f6c7cbf51244be8b14e313b97bfe32b1e127e55 + languageName: node + linkType: hard + "jest-worker@npm:^27.4.5, jest-worker@npm:^27.5.1": version: 27.5.1 resolution: "jest-worker@npm:27.5.1" @@ -13276,6 +15675,18 @@ __metadata: languageName: node linkType: hard +"jest-worker@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-worker@npm:29.7.0" + dependencies: + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + merge-stream: "npm:^2.0.0" + supports-color: "npm:^8.0.0" + checksum: 10/364cbaef00d8a2729fc760227ad34b5e60829e0869bd84976bdfbd8c0d0f9c2f22677b3e6dd8afa76ed174765351cd12bae3d4530c62eefb3791055127ca9745 + languageName: node + linkType: hard + "jest@npm:^27.5.1": version: 27.5.1 resolution: "jest@npm:27.5.1" @@ -13294,6 +15705,25 @@ __metadata: languageName: node linkType: hard +"jest@npm:^29.3.1": + version: 29.7.0 + resolution: "jest@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + import-local: "npm:^3.0.2" + jest-cli: "npm:^29.7.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10/97023d78446098c586faaa467fbf2c6b07ff06e2c85a19e3926adb5b0effe9ac60c4913ae03e2719f9c01ae8ffd8d92f6b262cedb9555ceeb5d19263d8c6362a + languageName: node + linkType: hard + "joi@npm:^17.4.0": version: 17.9.2 resolution: "joi@npm:17.9.2" @@ -13307,6 +15737,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^4.15.9": + version: 4.15.9 + resolution: "jose@npm:4.15.9" + checksum: 10/256234b6f85cdc080b1331f2d475bd58c8ccf459cb20f70ac5e4200b271bce10002b1c2f8e5b96dd975d83065ae5a586d52cdf89d28471d56de5d297992f9905 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -13393,6 +15830,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:^3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" + bin: + jsesc: bin/jsesc + checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 + languageName: node + linkType: hard + "jsesc@npm:~0.5.0": version: 0.5.0 resolution: "jsesc@npm:0.5.0" @@ -13541,6 +15987,27 @@ __metadata: languageName: node linkType: hard +"jwa@npm:^2.0.0": + version: 2.0.0 + resolution: "jwa@npm:2.0.0" + dependencies: + buffer-equal-constant-time: "npm:1.0.1" + ecdsa-sig-formatter: "npm:1.0.11" + safe-buffer: "npm:^5.0.1" + checksum: 10/ab983f6685d99d13ddfbffef9b1c66309a536362a8412d49ba6e687d834a1240ce39290f30ac7dbe241e0ab6c76fee7ff795776ce534e11d148158c9b7193498 + languageName: node + linkType: hard + +"jws@npm:^4.0.0": + version: 4.0.0 + resolution: "jws@npm:4.0.0" + dependencies: + jwa: "npm:^2.0.0" + safe-buffer: "npm:^5.0.1" + checksum: 10/1d15f4cdea376c6bd6a81002bd2cb0bf3d51d83da8f0727947b5ba3e10cf366721b8c0d099bf8c1eb99eb036e2c55e5fd5efd378ccff75a2b4e0bd10002348b9 + languageName: node + linkType: hard + "keyv@npm:^4.0.0": version: 4.5.2 resolution: "keyv@npm:4.5.2" @@ -13580,6 +16047,13 @@ __metadata: languageName: node linkType: hard +"kuler@npm:^2.0.0": + version: 2.0.0 + resolution: "kuler@npm:2.0.0" + checksum: 10/9e10b5a1659f9ed8761d38df3c35effabffbd19fc6107324095238e4ef0ff044392cae9ac64a1c2dda26e532426485342226b93806bd97504b174b0dcf04ed81 + languageName: node + linkType: hard + "language-subtag-registry@npm:^0.3.20": version: 0.3.23 resolution: "language-subtag-registry@npm:0.3.23" @@ -13790,6 +16264,20 @@ __metadata: languageName: node linkType: hard +"logform@npm:^2.6.0, logform@npm:^2.6.1": + version: 2.6.1 + resolution: "logform@npm:2.6.1" + dependencies: + "@colors/colors": "npm:1.6.0" + "@types/triple-beam": "npm:^1.3.2" + fecha: "npm:^4.2.0" + ms: "npm:^2.1.1" + safe-stable-stringify: "npm:^2.3.1" + triple-beam: "npm:^1.3.0" + checksum: 10/e67f414787fbfe1e6a997f4c84300c7e06bee3d0bd579778af667e24b36db3ea200ed195d41b61311ff738dab7faabc615a07b174b22fe69e0b2f39e985be64b + languageName: node + linkType: hard + "longest-streak@npm:^3.0.0": version: 3.1.0 resolution: "longest-streak@npm:3.1.0" @@ -13985,7 +16473,7 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:^3.0.0": +"make-dir@npm:^3.0.0, make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" dependencies: @@ -14301,6 +16789,13 @@ __metadata: languageName: node linkType: hard +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: 10/38e0984db39139604756903a01397e29e17dcb04207bb3e081412ce725ab17338ecc47220c1b186b6bbe79a658aad1b0d41142884f5a481f36290cdefbe6aa46 + languageName: node + linkType: hard + "memfs@npm:3.5.3": version: 3.5.3 resolution: "memfs@npm:3.5.3" @@ -14324,6 +16819,13 @@ __metadata: languageName: node linkType: hard +"merge-descriptors@npm:1.0.3": + version: 1.0.3 + resolution: "merge-descriptors@npm:1.0.3" + checksum: 10/52117adbe0313d5defa771c9993fe081e2d2df9b840597e966aadafde04ae8d0e3da46bac7ca4efc37d4d2b839436582659cd49c6a43eacb3fe3050896a105d1 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -14338,6 +16840,13 @@ __metadata: languageName: node linkType: hard +"methods@npm:^1.1.2, methods@npm:~1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 10/a385dd974faa34b5dd021b2bbf78c722881bf6f003bfe6d391d7da3ea1ed625d1ff10ddd13c57531f628b3e785be38d3eed10ad03cebd90b76932413df9a1820 + languageName: node + linkType: hard + "micromark-core-commonmark@npm:^1.0.0, micromark-core-commonmark@npm:^1.0.1": version: 1.1.0 resolution: "micromark-core-commonmark@npm:1.1.0" @@ -14677,6 +17186,29 @@ __metadata: languageName: node linkType: hard +"migrate@npm:^2.0.1": + version: 2.1.0 + resolution: "migrate@npm:2.1.0" + dependencies: + chalk: "npm:^4.1.2" + commander: "npm:^2.20.3" + dateformat: "npm:^4.6.3" + dotenv: "npm:^16.0.0" + inherits: "npm:^2.0.3" + minimatch: "npm:^9.0.1" + mkdirp: "npm:^3.0.1" + slug: "npm:^8.2.2" + bin: + migrate: bin/migrate + migrate-create: bin/migrate-create + migrate-down: bin/migrate-down + migrate-init: bin/migrate-init + migrate-list: bin/migrate-list + migrate-up: bin/migrate-up + checksum: 10/13aabd8f018053f8db079925b02da808efc196150e0f28d536752e4dab2c81d4cb9dc69fb54268bfa2b86e79ce5241a6b6514bcbdc9250a4d71e065f61f774ae + languageName: node + linkType: hard + "mime-db@npm:1.52.0, mime-db@npm:^1.28.0": version: 1.52.0 resolution: "mime-db@npm:1.52.0" @@ -14684,7 +17216,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -14693,7 +17225,16 @@ __metadata: languageName: node linkType: hard -"mime@npm:^2.5.2": +"mime@npm:1.6.0": + version: 1.6.0 + resolution: "mime@npm:1.6.0" + bin: + mime: cli.js + checksum: 10/b7d98bb1e006c0e63e2c91b590fe1163b872abf8f7ef224d53dd31499c2197278a6d3d0864c45239b1a93d22feaf6f9477e9fc847eef945838150b8c02d03170 + languageName: node + linkType: hard + +"mime@npm:2.6.0, mime@npm:^2.5.2": version: 2.6.0 resolution: "mime@npm:2.6.0" bin: @@ -14737,7 +17278,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.2, minimatch@npm:^3.0.3, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.2, minimatch@npm:^3.0.3, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -14893,6 +17434,15 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 10/16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba + languageName: node + linkType: hard + "mktemp@npm:~0.4.0": version: 0.4.0 resolution: "mktemp@npm:0.4.0" @@ -14947,7 +17497,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -14984,6 +17534,13 @@ __metadata: languageName: node linkType: hard +"natural-compare-lite@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare-lite@npm:1.4.0" + checksum: 10/5222ac3986a2b78dd6069ac62cbb52a7bf8ffc90d972ab76dfe7b01892485d229530ed20d0c62e79a6b363a663b273db3bde195a1358ce9e5f779d4453887225 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -14991,7 +17548,7 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^0.6.3": +"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: 10/2723fb822a17ad55c93a588a4bc44d53b22855bf4be5499916ca0cab1e7165409d0b288ba2577d7b029f10ce18cf2ed8e703e5af31c984e1e2304277ef979837 @@ -15040,6 +17597,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^5.0.0": + version: 5.1.0 + resolution: "node-addon-api@npm:5.1.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10/595f59ffb4630564f587c502119cbd980d302e482781021f3b479f5fc7e41cf8f2f7280fdc2795f32d148e4f3259bd15043c52d4a3442796aa6f1ae97b959636 + languageName: node + linkType: hard + "node-api-version@npm:^0.2.0": version: 0.2.0 resolution: "node-api-version@npm:0.2.0" @@ -15065,6 +17631,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.6.7": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10/b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 + languageName: node + linkType: hard + "node-fetch@npm:^3.3.2": version: 3.3.2 resolution: "node-fetch@npm:3.3.2" @@ -15145,6 +17725,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.18": + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: 10/241e5fa9556f1c12bafb83c6c3e94f8cf3d8f2f8f904906ecef6e10bcaa1d59aa61212d4651bec70052015fc54bd3fdcdbe7fc0f638a17e6685aa586c076ec4e + languageName: node + linkType: hard + "noms@npm:0.0.0": version: 0.0.0 resolution: "noms@npm:0.0.0" @@ -15155,6 +17742,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:^5.0.0": + version: 5.0.0 + resolution: "nopt@npm:5.0.0" + dependencies: + abbrev: "npm:1" + bin: + nopt: bin/nopt.js + checksum: 10/00f9bb2d16449469ba8ffcf9b8f0eae6bae285ec74b135fec533e5883563d2400c0cd70902d0a7759e47ac031ccf206ace4e86556da08ed3f1c66dda206e9ccd + languageName: node + linkType: hard + "nopt@npm:^6.0.0": version: 6.0.0 resolution: "nopt@npm:6.0.0" @@ -15166,6 +17764,16 @@ __metadata: languageName: node linkType: hard +"nordigen-node@npm:^1.4.0": + version: 1.4.0 + resolution: "nordigen-node@npm:1.4.0" + dependencies: + axios: "npm:^1.2.1" + dotenv: "npm:^10.0.0" + checksum: 10/88def53ba66468f4ac05ec12c6958eb87c20907a51bfc1f02b974ac3d9efcc61e67e4e9408c3d008703ad3ae9723564b5c08a627c4d3ac412cea4fb7ff50d6f2 + languageName: node + linkType: hard + "normalize-package-data@npm:^2.3.2": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -15259,6 +17867,18 @@ __metadata: languageName: node linkType: hard +"npmlog@npm:^5.0.1": + version: 5.0.1 + resolution: "npmlog@npm:5.0.1" + dependencies: + are-we-there-yet: "npm:^2.0.0" + console-control-strings: "npm:^1.1.0" + gauge: "npm:^3.0.0" + set-blocking: "npm:^2.0.0" + checksum: 10/f42c7b9584cdd26a13c41a21930b6f5912896b6419ab15be88cc5721fc792f1c3dd30eb602b26ae08575694628ba70afdcf3675d86e4f450fc544757e52726ec + languageName: node + linkType: hard + "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -15287,13 +17907,20 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": +"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f languageName: node linkType: hard +"object-hash@npm:^2.2.0": + version: 2.2.0 + resolution: "object-hash@npm:2.2.0" + checksum: 10/dee06b6271bf5769ae5f1a7386fdd52c1f18aae9fcb0b8d4bb1232f2d743d06cb5b662be42378b60a1c11829f96f3f86834a16bbaa57a085763295fff8b93e27 + languageName: node + linkType: hard + "object-inspect@npm:^1.13.1": version: 1.13.2 resolution: "object-inspect@npm:1.13.2" @@ -15382,6 +18009,29 @@ __metadata: languageName: node linkType: hard +"oidc-token-hash@npm:^5.0.3": + version: 5.0.3 + resolution: "oidc-token-hash@npm:5.0.3" + checksum: 10/35fa19aea9ff2c509029ec569d74b778c8a215b92bd5e6e9bc4ebbd7ab035f44304ff02430a6397c3fb7c1d15ebfa467807ca0bcd31d06ba610b47798287d303 + languageName: node + linkType: hard + +"on-finished@npm:2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: "npm:1.1.1" + checksum: 10/8e81472c5028125c8c39044ac4ab8ba51a7cdc19a9fbd4710f5d524a74c6d8c9ded4dd0eed83f28d3d33ac1d7a6a439ba948ccb765ac6ce87f30450a26bfe2ea + languageName: node + linkType: hard + +"on-headers@npm:1.0.1": + version: 1.0.1 + resolution: "on-headers@npm:1.0.1" + checksum: 10/7e5dc811cd8e16590385ac56a63e27aa9006c594069960655d37f96c9b1f7af4ce7e71f4f6a771ed746ebf180e0c1cbc1e5ecd125a4006b9eb0ef23125068cd2 + languageName: node + linkType: hard + "once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -15391,6 +18041,15 @@ __metadata: languageName: node linkType: hard +"one-time@npm:^1.0.0": + version: 1.0.0 + resolution: "one-time@npm:1.0.0" + dependencies: + fn.name: "npm:1.x.x" + checksum: 10/64d0160480eeae4e3b2a6fc0a02f452e05bb0cc8373a4ed56a4fc08c3939dcb91bc20075003ed499655bd16919feb63ca56f86eee7932c5251f7d629b55dfc90 + languageName: node + linkType: hard + "onetime@npm:^5.1.0, onetime@npm:^5.1.2": version: 5.1.2 resolution: "onetime@npm:5.1.2" @@ -15438,6 +18097,18 @@ __metadata: languageName: node linkType: hard +"openid-client@npm:^5.4.2": + version: 5.7.1 + resolution: "openid-client@npm:5.7.1" + dependencies: + jose: "npm:^4.15.9" + lru-cache: "npm:^6.0.0" + object-hash: "npm:^2.2.0" + oidc-token-hash: "npm:^5.0.3" + checksum: 10/188a875ab1824010bde85b6755f31401d4b0bcf6edffe5f149b1e67fc886c692658121c0c3cc04db84be33138c0e9e2e7d829e6997adf489f23a32ea7e745151 + languageName: node + linkType: hard + "optionator@npm:^0.8.1": version: 0.8.3 resolution: "optionator@npm:0.8.3" @@ -15522,7 +18193,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2": +"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -15663,6 +18334,13 @@ __metadata: languageName: node linkType: hard +"parseurl@npm:~1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 10/407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 + languageName: node + linkType: hard + "path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -15729,6 +18407,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:0.1.10": + version: 0.1.10 + resolution: "path-to-regexp@npm:0.1.10" + checksum: 10/894e31f1b20e592732a87db61fff5b95c892a3fe430f9ab18455ebe69ee88ef86f8eb49912e261f9926fc53da9f93b46521523e33aefd9cb0a7b0d85d7096006 + languageName: node + linkType: hard + "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -15988,7 +18673,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.8.7": +"prettier@npm:^2.8.3, prettier@npm:^2.8.7": version: 2.8.8 resolution: "prettier@npm:2.8.8" bin: @@ -16108,6 +18793,15 @@ __metadata: languageName: node linkType: hard +"properties-reader@npm:^2.2.0": + version: 2.3.0 + resolution: "properties-reader@npm:2.3.0" + dependencies: + mkdirp: "npm:^1.0.4" + checksum: 10/0b41eb4136dc278ae0d97968ccce8de2d48d321655b319192e31f2424f1c6e052182204671e65aa8967216360cb3e7cbd9129830062e058fe9d6a1d74964c29a + languageName: node + linkType: hard + "property-information@npm:^6.0.0": version: 6.2.0 resolution: "property-information@npm:6.2.0" @@ -16122,6 +18816,23 @@ __metadata: languageName: node linkType: hard +"proxy-addr@npm:~2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: "npm:0.2.0" + ipaddr.js: "npm:1.9.1" + checksum: 10/f24a0c80af0e75d31e3451398670d73406ec642914da11a2965b80b1898ca6f66a0e3e091a11a4327079b2b268795f6fa06691923fef91887215c3d0e8ea3f68 + languageName: node + linkType: hard + +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: 10/f0bb4a87cfd18f77bc2fba23ae49c3b378fb35143af16cc478171c623eebe181678f09439707ad80081d340d1593cd54a33a0113f3ccb3f4bc9451488780ee23 + languageName: node + linkType: hard + "pseudomap@npm:^1.0.2": version: 1.0.2 resolution: "pseudomap@npm:1.0.2" @@ -16160,6 +18871,24 @@ __metadata: languageName: node linkType: hard +"qs@npm:6.11.0": + version: 6.11.0 + resolution: "qs@npm:6.11.0" + dependencies: + side-channel: "npm:^1.0.4" + checksum: 10/5a3bfea3e2f359ede1bfa5d2f0dbe54001aa55e40e27dc3e60fab814362d83a9b30758db057c2011b6f53a2d4e4e5150194b5bac45372652aecb3e3c0d4b256e + languageName: node + linkType: hard + +"qs@npm:6.13.0, qs@npm:^6.11.0": + version: 6.13.0 + resolution: "qs@npm:6.13.0" + dependencies: + side-channel: "npm:^1.0.6" + checksum: 10/f548b376e685553d12e461409f0d6e5c59ec7c7d76f308e2a888fd9db3e0c5e89902bedd0754db3a9038eda5f27da2331a6f019c8517dc5e0a16b3c9a6e9cef8 + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -16215,6 +18944,25 @@ __metadata: languageName: node linkType: hard +"range-parser@npm:~1.2.1": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 10/ce21ef2a2dd40506893157970dc76e835c78cf56437e26e19189c48d5291e7279314477b06ac38abd6a401b661a6840f7b03bd0b1249da9b691deeaa15872c26 + languageName: node + linkType: hard + +"raw-body@npm:2.5.2": + version: 2.5.2 + resolution: "raw-body@npm:2.5.2" + dependencies: + bytes: "npm:3.1.2" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.4.24" + unpipe: "npm:1.0.0" + checksum: 10/863b5171e140546a4d99f349b720abac4410338e23df5e409cfcc3752538c9caf947ce382c89129ba976f71894bd38b5806c774edac35ebf168d02aa1ac11a95 + languageName: node + linkType: hard + "rc4@npm:~0.1.5": version: 0.1.5 resolution: "rc4@npm:0.1.5" @@ -16748,6 +19496,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.5.2": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10/01b128a559c5fd76a898495f858cf0a8839f135e6a69e3409f986e88460134791657eb46a2ff16826f331682a3c4d0c5a75cef5e52ef259711021ba52b1c2e82 + languageName: node + linkType: hard + "readable-stream@npm:~1.0.31": version: 1.0.34 resolution: "readable-stream@npm:1.0.34" @@ -17099,6 +19860,13 @@ __metadata: languageName: node linkType: hard +"resolve.exports@npm:^2.0.0": + version: 2.0.2 + resolution: "resolve.exports@npm:2.0.2" + checksum: 10/f1cc0b6680f9a7e0345d783e0547f2a5110d8336b3c2a4227231dd007271ffd331fd722df934f017af90bae0373920ca0d4005da6f76cb3176c8ae426370f893 + languageName: node + linkType: hard + "resolve@npm:^1.10.0, resolve@npm:^1.10.1, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4": version: 1.22.8 resolution: "resolve@npm:1.22.8" @@ -17397,6 +20165,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.3.1": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -17499,7 +20274,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.3": +"semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -17508,6 +20283,48 @@ __metadata: languageName: node linkType: hard +"send@npm:0.18.0": + version: 0.18.0 + resolution: "send@npm:0.18.0" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:2.0.1" + checksum: 10/ec66c0ad109680ad8141d507677cfd8b4e40b9559de23191871803ed241718e99026faa46c398dcfb9250676076573bd6bfe5d0ec347f88f4b7b8533d1d391cb + languageName: node + linkType: hard + +"send@npm:0.19.0": + version: 0.19.0 + resolution: "send@npm:0.19.0" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:2.0.1" + checksum: 10/1f6064dea0ae4cbe4878437aedc9270c33f2a6650a77b56a16b62d057527f2766d96ee282997dd53ec0339082f2aad935bc7d989b46b48c82fc610800dc3a1d0 + languageName: node + linkType: hard + "serialize-error@npm:^7.0.1": version: 7.0.1 resolution: "serialize-error@npm:7.0.1" @@ -17526,6 +20343,18 @@ __metadata: languageName: node linkType: hard +"serve-static@npm:1.16.0": + version: 1.16.0 + resolution: "serve-static@npm:1.16.0" + dependencies: + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + parseurl: "npm:~1.3.3" + send: "npm:0.18.0" + checksum: 10/29a01f67e8c64a359d49dd0c46bc95bb4aa99781f97845dccbf0c8cd0284c5fd79ad7fb9433a36fac4b6c58b577d3eab314a379142412413b8b5cd73be3cd551 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -17559,6 +20388,13 @@ __metadata: languageName: node linkType: hard +"setprototypeof@npm:1.2.0": + version: 1.2.0 + resolution: "setprototypeof@npm:1.2.0" + checksum: 10/fde1630422502fbbc19e6844346778f99d449986b2f9cdcceb8326730d2f3d9964dbcb03c02aaadaefffecd0f2c063315ebea8b3ad895914bf1afc1747fc172e + languageName: node + linkType: hard + "shallow-clone@npm:^3.0.0": version: 3.0.1 resolution: "shallow-clone@npm:3.0.1" @@ -17658,6 +20494,15 @@ __metadata: languageName: node linkType: hard +"simple-swizzle@npm:^0.2.2": + version: 0.2.2 + resolution: "simple-swizzle@npm:0.2.2" + dependencies: + is-arrayish: "npm:^0.3.1" + checksum: 10/c6dffff17aaa383dae7e5c056fbf10cf9855a9f79949f20ee225c04f06ddde56323600e0f3d6797e82d08d006e93761122527438ee9531620031c08c9e0d73cc + languageName: node + linkType: hard + "simple-update-notifier@npm:2.0.0": version: 2.0.0 resolution: "simple-update-notifier@npm:2.0.0" @@ -17730,6 +20575,15 @@ __metadata: languageName: node linkType: hard +"slug@npm:^8.2.2": + version: 8.2.3 + resolution: "slug@npm:8.2.3" + bin: + slug: cli.js + checksum: 10/341e87b07fd89e2947cb7016e02fdf7a2280536b88715ba5318aa18dc4bfb18052037da867c512999d8cd7b14a50743c78f6604ae2d4aa9d3b0f289043ed5c3c + languageName: node + linkType: hard + "smart-buffer@npm:^4.0.2, smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -17829,6 +20683,16 @@ __metadata: languageName: node linkType: hard +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 10/d1514a922ac9c7e4786037eeff6c3322f461cd25da34bb9fefb15387b3490531774e6e31d95ab6d5b84a3e139af9c3a570ccaee6b47bd7ea262691ed3a8bc34e + languageName: node + linkType: hard + "source-map-support@npm:^0.5.19, source-map-support@npm:^0.5.21, source-map-support@npm:^0.5.6, source-map-support@npm:~0.5.20": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" @@ -17956,6 +20820,13 @@ __metadata: languageName: node linkType: hard +"stack-trace@npm:0.0.x": + version: 0.0.10 + resolution: "stack-trace@npm:0.0.10" + checksum: 10/7bd633f0e9ac46e81a0b0fe6538482c1d77031959cf94478228731709db4672fbbed59176f5b9a9fd89fec656b5dae03d084ef2d1b0c4c2f5683e05f2dbb1405 + languageName: node + linkType: hard + "stack-utils@npm:^2.0.3": version: 2.0.6 resolution: "stack-utils@npm:2.0.6" @@ -17979,6 +20850,13 @@ __metadata: languageName: node linkType: hard +"statuses@npm:2.0.1": + version: 2.0.1 + resolution: "statuses@npm:2.0.1" + checksum: 10/18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb + languageName: node + linkType: hard + "std-env@npm:^3.5.0": version: 3.6.0 resolution: "std-env@npm:3.6.0" @@ -18175,7 +21053,7 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.1.1": +"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" dependencies: @@ -18337,6 +21215,34 @@ __metadata: languageName: node linkType: hard +"superagent@npm:^8.1.2": + version: 8.1.2 + resolution: "superagent@npm:8.1.2" + dependencies: + component-emitter: "npm:^1.3.0" + cookiejar: "npm:^2.1.4" + debug: "npm:^4.3.4" + fast-safe-stringify: "npm:^2.1.1" + form-data: "npm:^4.0.0" + formidable: "npm:^2.1.2" + methods: "npm:^1.1.2" + mime: "npm:2.6.0" + qs: "npm:^6.11.0" + semver: "npm:^7.3.8" + checksum: 10/33d0072e051baf91c7d68131c70682a0650dd1bd0b8dfb6f88e5bdfcb02e18cc2b42a66e44b32fd405ac6bcf5fd57c6e267bf80e2a8ce57a18166a9d3a78f57d + languageName: node + linkType: hard + +"supertest@npm:^6.3.1": + version: 6.3.4 + resolution: "supertest@npm:6.3.4" + dependencies: + methods: "npm:^1.1.2" + superagent: "npm:^8.1.2" + checksum: 10/93015318f5a90398915a032747973d9eacf9aebec3f07b413eba9d8b3db83ff48fbf6f5a92f9526578cae50153b0f76a37de197141030d856db4371a711b86ee + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -18632,6 +21538,20 @@ __metadata: languageName: node linkType: hard +"text-hex@npm:1.0.x": + version: 1.0.0 + resolution: "text-hex@npm:1.0.0" + checksum: 10/1138f68adc97bf4381a302a24e2352f04992b7b1316c5003767e9b0d3367ffd0dc73d65001ea02b07cd0ecc2a9d186de0cf02f3c2d880b8a522d4ccb9342244a + languageName: node + linkType: hard + +"text-table@npm:^0.2.0": + version: 0.2.0 + resolution: "text-table@npm:0.2.0" + checksum: 10/4383b5baaeffa9bb4cda2ac33a4aa2e6d1f8aaf811848bf73513a9b88fd76372dc461f6fd6d2e9cb5100f48b473be32c6f95bd983509b7d92bb4d92c10747452 + languageName: node + linkType: hard + "throat@npm:^6.0.1": version: 6.0.2 resolution: "throat@npm:6.0.2" @@ -18755,6 +21675,13 @@ __metadata: languageName: node linkType: hard +"toidentifier@npm:1.0.1": + version: 1.0.1 + resolution: "toidentifier@npm:1.0.1" + checksum: 10/952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 + languageName: node + linkType: hard + "totalist@npm:^3.0.0": version: 3.0.1 resolution: "totalist@npm:3.0.1" @@ -18792,6 +21719,13 @@ __metadata: languageName: node linkType: hard +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 10/8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695 + languageName: node + linkType: hard + "trampa@npm:^1.0.0": version: 1.0.1 resolution: "trampa@npm:1.0.1" @@ -18815,6 +21749,13 @@ __metadata: languageName: node linkType: hard +"triple-beam@npm:^1.3.0": + version: 1.4.1 + resolution: "triple-beam@npm:1.4.1" + checksum: 10/2e881a3e8e076b6f2b85b9ec9dd4a900d3f5016e6d21183ed98e78f9abcc0149e7d54d79a3f432b23afde46b0885bdcdcbff789f39bc75de796316961ec07f61 + languageName: node + linkType: hard + "trough@npm:^2.0.0": version: 2.1.0 resolution: "trough@npm:2.1.0" @@ -18931,6 +21872,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^1.8.1": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb + languageName: node + linkType: hard + "tslib@npm:^2.0.3, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" @@ -18938,6 +21886,17 @@ __metadata: languageName: node linkType: hard +"tsutils@npm:^3.21.0": + version: 3.21.0 + resolution: "tsutils@npm:3.21.0" + dependencies: + tslib: "npm:^1.8.1" + peerDependencies: + typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + checksum: 10/ea036bec1dd024e309939ffd49fda7a351c0e87a1b8eb049570dd119d447250e2c56e0e6c00554e8205760e7417793fdebff752a46e573fbe07d4f375502a5b2 + languageName: node + linkType: hard + "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -18986,6 +21945,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.20.2": + version: 0.20.2 + resolution: "type-fest@npm:0.20.2" + checksum: 10/8907e16284b2d6cfa4f4817e93520121941baba36b39219ea36acfe64c86b9dbc10c9941af450bd60832c8f43464974d51c0957f9858bc66b952b66b6914cbb9 + languageName: node + linkType: hard + "type-fest@npm:^0.21.3": version: 0.21.3 resolution: "type-fest@npm:0.21.3" @@ -19002,6 +21968,16 @@ __metadata: languageName: node linkType: hard +"type-is@npm:~1.6.18": + version: 1.6.18 + resolution: "type-is@npm:1.6.18" + dependencies: + media-typer: "npm:0.3.0" + mime-types: "npm:~2.1.24" + checksum: 10/0bd9eeae5efd27d98fd63519f999908c009e148039d8e7179a074f105362d4fcc214c38b24f6cda79c87e563cbd12083a4691381ed28559220d4a10c2047bed4 + languageName: node + linkType: hard + "typed-array-buffer@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-buffer@npm:1.0.2" @@ -19093,7 +22069,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^4.0.2": +"typescript@npm:^4.0.2, typescript@npm:^4.9.5": version: 4.9.5 resolution: "typescript@npm:4.9.5" bin: @@ -19113,7 +22089,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^4.0.2#optional!builtin": +"typescript@patch:typescript@npm%3A^4.0.2#optional!builtin, typescript@patch:typescript@npm%3A^4.9.5#optional!builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin::version=4.9.5&hash=289587" bin: @@ -19399,6 +22375,13 @@ __metadata: languageName: node linkType: hard +"unpipe@npm:1.0.0, unpipe@npm:~1.0.0": + version: 1.0.0 + resolution: "unpipe@npm:1.0.0" + checksum: 10/4fa18d8d8d977c55cb09715385c203197105e10a6d220087ec819f50cb68870f02942244f1017565484237f1f8c5d3cd413631b1ae104d3096f24fdfde1b4aa2 + languageName: node + linkType: hard + "untildify@npm:^4.0.0": version: 4.0.0 resolution: "untildify@npm:4.0.0" @@ -19427,6 +22410,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.1.1": + version: 1.1.1 + resolution: "update-browserslist-db@npm:1.1.1" + dependencies: + escalade: "npm:^3.2.0" + picocolors: "npm:^1.1.0" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10/7678dd8609750588d01aa7460e8eddf2ff9d16c2a52fb1811190e0d056390f1fdffd94db3cf8fb209cf634ab4fa9407886338711c71cc6ccade5eeb22b093734 + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -19518,6 +22515,13 @@ __metadata: languageName: node linkType: hard +"utils-merge@npm:1.0.1": + version: 1.0.1 + resolution: "utils-merge@npm:1.0.1" + checksum: 10/5d6949693d58cb2e636a84f3ee1c6e7b2f9c16cb1d42d0ecb386d8c025c69e327205aa1c69e2868cc06a01e5e20681fbba55a4e0ed0cce913d60334024eae798 + languageName: node + linkType: hard + "uuid@npm:^3.0.1, uuid@npm:^3.3.2": version: 3.4.0 resolution: "uuid@npm:3.4.0" @@ -19527,7 +22531,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.1": +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: @@ -19568,6 +22572,17 @@ __metadata: languageName: node linkType: hard +"v8-to-istanbul@npm:^9.0.1": + version: 9.3.0 + resolution: "v8-to-istanbul@npm:9.3.0" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.12" + "@types/istanbul-lib-coverage": "npm:^2.0.1" + convert-source-map: "npm:^2.0.0" + checksum: 10/fb1d70f1176cb9dc46cabbb3fd5c52c8f3e8738b61877b6e7266029aed0870b04140e3f9f4550ac32aebcfe1d0f38b0bac57e1e8fb97d68fec82f2b416148166 + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.1": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" @@ -19585,6 +22600,13 @@ __metadata: languageName: node linkType: hard +"vary@npm:^1, vary@npm:~1.1.2": + version: 1.1.2 + resolution: "vary@npm:1.1.2" + checksum: 10/31389debef15a480849b8331b220782230b9815a8e0dbb7b9a8369559aed2e9a7800cd904d4371ea74f4c3527db456dc8e7ac5befce5f0d289014dbdf47b2242 + languageName: node + linkType: hard + "verror@npm:^1.10.0": version: 1.10.1 resolution: "verror@npm:1.10.1" @@ -19925,6 +22947,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10/b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad + languageName: node + linkType: hard + "webidl-conversions@npm:^4.0.2": version: 4.0.2 resolution: "webidl-conversions@npm:4.0.2" @@ -20070,6 +23099,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10/f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 + languageName: node + linkType: hard + "whatwg-url@npm:^7.0.0": version: 7.1.0 resolution: "whatwg-url@npm:7.1.0" @@ -20211,7 +23250,7 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.5": +"wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" dependencies: @@ -20227,6 +23266,36 @@ __metadata: languageName: node linkType: hard +"winston-transport@npm:^4.7.0": + version: 4.8.0 + resolution: "winston-transport@npm:4.8.0" + dependencies: + logform: "npm:^2.6.1" + readable-stream: "npm:^4.5.2" + triple-beam: "npm:^1.3.0" + checksum: 10/930bdc0ec689d5c4f07a262721da80440336f64739d0ce33db801c7142b4fca5be8ef71b725b670bac609de8b6bce405e5c5f84d355f5176a611209b476cee18 + languageName: node + linkType: hard + +"winston@npm:^3.14.2": + version: 3.15.0 + resolution: "winston@npm:3.15.0" + dependencies: + "@colors/colors": "npm:^1.6.0" + "@dabh/diagnostics": "npm:^2.0.2" + async: "npm:^3.2.3" + is-stream: "npm:^2.0.0" + logform: "npm:^2.6.0" + one-time: "npm:^1.0.0" + readable-stream: "npm:^3.4.0" + safe-stable-stringify: "npm:^2.3.1" + stack-trace: "npm:0.0.x" + triple-beam: "npm:^1.3.0" + winston-transport: "npm:^4.7.0" + checksum: 10/60e55eb3621e4de1a764a4e43ee1d242c71957d3e0eb359cb8f16fe2b9d9543fd4c31a8d3baf96fa7e43ef5df383c43c1a98aff4bd714ea0082303504b0e3cdc + languageName: node + linkType: hard + "word-wrap@npm:~1.2.3": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -20626,7 +23695,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.0.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": +"yargs@npm:^17.0.1, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: