@actual-app/sync-server 26.10.0 → 26.11.0-nightly.20261004

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as run } from "./chunks/migrations-7U65Wxd5.js";
2
2
  //#region app.ts
3
3
  run().then(() => {
4
- import("./chunks/app-BgMEdvse.js").then((app) => app.run());
4
+ import("./chunks/app-C9cIy7Ri.js").then((app) => app.run());
5
5
  }).catch((err) => {
6
6
  console.log("Error starting app:", err);
7
7
  process.exit(1);
@@ -41,7 +41,7 @@ if (values.help) {
41
41
  process.exit();
42
42
  }
43
43
  if (values.version) {
44
- console.log("v26.10.0");
44
+ console.log("v26.11.0-nightly.20261004");
45
45
  process.exit();
46
46
  }
47
47
  var setupDataDir = (dataDir) => {
@@ -1 +1 @@
1
- {"version":3,"file":"actual-server.js","names":[],"sources":["../../bin/actual-server.js"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parseArgs } from 'node:util';\n\nconst args = process.argv;\n\nconst options = {\n help: {\n type: 'boolean',\n short: 'h',\n },\n version: {\n type: 'boolean',\n short: 'v',\n },\n 'reset-password': {\n type: 'boolean',\n },\n config: {\n type: 'string',\n },\n};\n\nconst { values } = parseArgs({\n args,\n options,\n allowPositionals: true,\n});\n\nif (values.help) {\n console.log(\n [\n 'usage: actual-server [options]',\n '',\n 'options:',\n ' --config Path to config file',\n '',\n ' -h --help Print this list and exit.',\n ' -v --version Print the version and exit.',\n '',\n 'Examples:',\n '',\n 'Runs actual-server with default configuration',\n ' actual-server',\n '',\n 'Runs actual-server with custom configuration',\n ' actual-server --config ./config.json',\n ].join('\\n'),\n );\n\n process.exit();\n}\n\nif (values.version) {\n console.log('v' + __APP_VERSION__);\n process.exit();\n}\n\nconst setupDataDir = dataDir => {\n if (process.env.ACTUAL_DATA_DIR) {\n return; // Env variables must not be overwritten\n }\n\n if (dataDir) {\n process.env.ACTUAL_DATA_DIR = dataDir; // Use the dir specified\n } else {\n // Setup defaults\n if (existsSync('./data')) {\n // The default data directory exists - use it\n console.info('Found existing data directory');\n process.env.ACTUAL_DATA_DIR = resolve('./data');\n } else {\n console.info(\n 'Using default data directory. You can specify a custom config with --config',\n );\n process.env.ACTUAL_DATA_DIR = resolve('./');\n }\n\n console.info(`Data directory: ${process.env.ACTUAL_DATA_DIR}`);\n }\n};\n\nif (values.config) {\n const configExists = existsSync(values.config);\n\n if (!configExists) {\n console.log(\n `Please specify a valid config path. The path ${String(values.config)} does not exist.`,\n );\n\n process.exit();\n } else {\n console.log(`Loading config from ${String(values.config)}`);\n const configJson = JSON.parse(readFileSync(values.config, 'utf-8'));\n process.env.ACTUAL_CONFIG_PATH = values.config;\n setupDataDir(configJson.dataDir);\n }\n} else {\n // If no config is specified, check for a default config in the current directory\n const defaultConfigJsonFile = './config.json';\n const configExists = existsSync(defaultConfigJsonFile);\n\n if (configExists) {\n console.info('Found config.json in the current directory');\n const configJson = JSON.parse(readFileSync(defaultConfigJsonFile, 'utf-8'));\n process.env.ACTUAL_CONFIG_PATH = defaultConfigJsonFile;\n setupDataDir(configJson.dataDir);\n } else {\n setupDataDir(); // No default config exists - setup data dir with defaults\n }\n}\n\nif (values['reset-password']) {\n console.info('Running reset password script...');\n await import('../src/scripts/reset-password.js');\n process.exit();\n}\n\n// start the sync server\nvoid import('../app.js');\n"],"mappings":";;;;;AAKA,IAAM,OAAO,QAAQ;AAmBrB,IAAM,EAAE,WAAW,UAAU;CAC3B;CACA,SAAA;EAlBA,MAAM;GACJ,MAAM;GACN,OAAO;EACT;EACA,SAAS;GACP,MAAM;GACN,OAAO;EACT;EACA,kBAAkB,EAChB,MAAM,UACR;EACA,QAAQ,EACN,MAAM,SACR;CAKM;CACN,kBAAkB;AACpB,CAAC;AAED,IAAI,OAAO,MAAM;CACf,QAAQ,IACN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC,CAAC,KAAK,IAAI,CACb;CAEA,QAAQ,KAAK;AACf;AAEA,IAAI,OAAO,SAAS;CAClB,QAAQ,IAAI,UAAqB;CACjC,QAAQ,KAAK;AACf;AAEA,IAAM,gBAAe,YAAW;CAC9B,IAAI,QAAQ,IAAI,iBACd;CAGF,IAAI,SACF,QAAQ,IAAI,kBAAkB;MACzB;EAEL,IAAI,WAAW,QAAQ,GAAG;GAExB,QAAQ,KAAK,+BAA+B;GAC5C,QAAQ,IAAI,kBAAkB,QAAQ,QAAQ;EAChD,OAAO;GACL,QAAQ,KACN,6EACF;GACA,QAAQ,IAAI,kBAAkB,QAAQ,IAAI;EAC5C;EAEA,QAAQ,KAAK,mBAAmB,QAAQ,IAAI,iBAAiB;CAC/D;AACF;AAEA,IAAI,OAAO,QAGT,IAAI,CAFiB,WAAW,OAAO,MAEvB,GAAG;CACjB,QAAQ,IACN,gDAAgD,OAAO,OAAO,MAAM,EAAE,iBACxE;CAEA,QAAQ,KAAK;AACf,OAAO;CACL,QAAQ,IAAI,uBAAuB,OAAO,OAAO,MAAM,GAAG;CAC1D,MAAM,aAAa,KAAK,MAAM,aAAa,OAAO,QAAQ,OAAO,CAAC;CAClE,QAAQ,IAAI,qBAAqB,OAAO;CACxC,aAAa,WAAW,OAAO;AACjC;KACK;CAEL,MAAM,wBAAwB;CAG9B,IAFqB,WAAW,qBAEjB,GAAG;EAChB,QAAQ,KAAK,4CAA4C;EACzD,MAAM,aAAa,KAAK,MAAM,aAAa,uBAAuB,OAAO,CAAC;EAC1E,QAAQ,IAAI,qBAAqB;EACjC,aAAa,WAAW,OAAO;CACjC,OACE,aAAa;AAEjB;AAEA,IAAI,OAAO,mBAAmB;CAC5B,QAAQ,KAAK,kCAAkC;CAC/C,MAAM,OAAO;CACb,QAAQ,KAAK;AACf;AAGK,OAAO"}
1
+ {"version":3,"file":"actual-server.js","names":[],"sources":["../../bin/actual-server.js"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parseArgs } from 'node:util';\n\nconst args = process.argv;\n\nconst options = {\n help: {\n type: 'boolean',\n short: 'h',\n },\n version: {\n type: 'boolean',\n short: 'v',\n },\n 'reset-password': {\n type: 'boolean',\n },\n config: {\n type: 'string',\n },\n};\n\nconst { values } = parseArgs({\n args,\n options,\n allowPositionals: true,\n});\n\nif (values.help) {\n console.log(\n [\n 'usage: actual-server [options]',\n '',\n 'options:',\n ' --config Path to config file',\n '',\n ' -h --help Print this list and exit.',\n ' -v --version Print the version and exit.',\n '',\n 'Examples:',\n '',\n 'Runs actual-server with default configuration',\n ' actual-server',\n '',\n 'Runs actual-server with custom configuration',\n ' actual-server --config ./config.json',\n ].join('\\n'),\n );\n\n process.exit();\n}\n\nif (values.version) {\n console.log('v' + __APP_VERSION__);\n process.exit();\n}\n\nconst setupDataDir = dataDir => {\n if (process.env.ACTUAL_DATA_DIR) {\n return; // Env variables must not be overwritten\n }\n\n if (dataDir) {\n process.env.ACTUAL_DATA_DIR = dataDir; // Use the dir specified\n } else {\n // Setup defaults\n if (existsSync('./data')) {\n // The default data directory exists - use it\n console.info('Found existing data directory');\n process.env.ACTUAL_DATA_DIR = resolve('./data');\n } else {\n console.info(\n 'Using default data directory. You can specify a custom config with --config',\n );\n process.env.ACTUAL_DATA_DIR = resolve('./');\n }\n\n console.info(`Data directory: ${process.env.ACTUAL_DATA_DIR}`);\n }\n};\n\nif (values.config) {\n const configExists = existsSync(values.config);\n\n if (!configExists) {\n console.log(\n `Please specify a valid config path. The path ${String(values.config)} does not exist.`,\n );\n\n process.exit();\n } else {\n console.log(`Loading config from ${String(values.config)}`);\n const configJson = JSON.parse(readFileSync(values.config, 'utf-8'));\n process.env.ACTUAL_CONFIG_PATH = values.config;\n setupDataDir(configJson.dataDir);\n }\n} else {\n // If no config is specified, check for a default config in the current directory\n const defaultConfigJsonFile = './config.json';\n const configExists = existsSync(defaultConfigJsonFile);\n\n if (configExists) {\n console.info('Found config.json in the current directory');\n const configJson = JSON.parse(readFileSync(defaultConfigJsonFile, 'utf-8'));\n process.env.ACTUAL_CONFIG_PATH = defaultConfigJsonFile;\n setupDataDir(configJson.dataDir);\n } else {\n setupDataDir(); // No default config exists - setup data dir with defaults\n }\n}\n\nif (values['reset-password']) {\n console.info('Running reset password script...');\n await import('../src/scripts/reset-password.js');\n process.exit();\n}\n\n// start the sync server\nvoid import('../app.js');\n"],"mappings":";;;;;AAKA,IAAM,OAAO,QAAQ;AAmBrB,IAAM,EAAE,WAAW,UAAU;CAC3B;CACA,SAAA;EAlBA,MAAM;GACJ,MAAM;GACN,OAAO;EACT;EACA,SAAS;GACP,MAAM;GACN,OAAO;EACT;EACA,kBAAkB,EAChB,MAAM,UACR;EACA,QAAQ,EACN,MAAM,SACR;CAKM;CACN,kBAAkB;AACpB,CAAC;AAED,IAAI,OAAO,MAAM;CACf,QAAQ,IACN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC,CAAC,KAAK,IAAI,CACb;CAEA,QAAQ,KAAK;AACf;AAEA,IAAI,OAAO,SAAS;CAClB,QAAQ,IAAI,2BAAqB;CACjC,QAAQ,KAAK;AACf;AAEA,IAAM,gBAAe,YAAW;CAC9B,IAAI,QAAQ,IAAI,iBACd;CAGF,IAAI,SACF,QAAQ,IAAI,kBAAkB;MACzB;EAEL,IAAI,WAAW,QAAQ,GAAG;GAExB,QAAQ,KAAK,+BAA+B;GAC5C,QAAQ,IAAI,kBAAkB,QAAQ,QAAQ;EAChD,OAAO;GACL,QAAQ,KACN,6EACF;GACA,QAAQ,IAAI,kBAAkB,QAAQ,IAAI;EAC5C;EAEA,QAAQ,KAAK,mBAAmB,QAAQ,IAAI,iBAAiB;CAC/D;AACF;AAEA,IAAI,OAAO,QAGT,IAAI,CAFiB,WAAW,OAAO,MAEvB,GAAG;CACjB,QAAQ,IACN,gDAAgD,OAAO,OAAO,MAAM,EAAE,iBACxE;CAEA,QAAQ,KAAK;AACf,OAAO;CACL,QAAQ,IAAI,uBAAuB,OAAO,OAAO,MAAM,GAAG;CAC1D,MAAM,aAAa,KAAK,MAAM,aAAa,OAAO,QAAQ,OAAO,CAAC;CAClE,QAAQ,IAAI,qBAAqB,OAAO;CACxC,aAAa,WAAW,OAAO;AACjC;KACK;CAEL,MAAM,wBAAwB;CAG9B,IAFqB,WAAW,qBAEjB,GAAG;EAChB,QAAQ,KAAK,4CAA4C;EACzD,MAAM,aAAa,KAAK,MAAM,aAAa,uBAAuB,OAAO,CAAC;EAC1E,QAAQ,IAAI,qBAAqB;EACjC,aAAa,WAAW,OAAO;CACjC,OACE,aAAa;AAEjB;AAEA,IAAI,OAAO,mBAAmB;CAC5B,QAAQ,KAAK,kCAAkC;CAC/C,MAAM,OAAO;CACb,QAAQ,KAAK;AACf;AAGK,OAAO"}
@@ -1280,7 +1280,7 @@ var enableBankingService = {
1280
1280
  async getAspsps(country) {
1281
1281
  return request("GET", `/aspsps${country ? `?country=${encodeURIComponent(country)}` : ""}`);
1282
1282
  },
1283
- async startAuth(aspsp, redirectUrl, state, maxConsentValidity, psuType = "personal") {
1283
+ async startAuth(aspsp, redirectUrl, state, maxConsentValidity, psuType = "personal", psuHeaders) {
1284
1284
  const defaultMs = 2160 * 60 * 60 * 1e3;
1285
1285
  const consentMs = maxConsentValidity != null && maxConsentValidity > 0 ? Math.min(maxConsentValidity * 1e3, defaultMs) : defaultMs;
1286
1286
  const validUntil = new Date(Date.now() + consentMs);
@@ -1291,9 +1291,13 @@ var enableBankingService = {
1291
1291
  },
1292
1292
  redirect_url: redirectUrl,
1293
1293
  state,
1294
- access: { valid_until: validUntil.toISOString() },
1294
+ access: {
1295
+ valid_until: validUntil.toISOString(),
1296
+ balances: true,
1297
+ transactions: true
1298
+ },
1295
1299
  psu_type: psuType
1296
- });
1300
+ }, void 0, psuHeaders);
1297
1301
  },
1298
1302
  async createSession(code) {
1299
1303
  return request("POST", "/sessions", { code });
@@ -1316,8 +1320,8 @@ var enableBankingService = {
1316
1320
  let iteration = 0;
1317
1321
  do {
1318
1322
  const result = await enableBankingService.getTransactions(accountUid, dateFrom, dateTo, continuationKey, psuHeaders);
1319
- allTransactions.push(...result.transactions);
1320
1323
  if (result.continuation_key && result.continuation_key === continuationKey) break;
1324
+ allTransactions.push(...result.transactions);
1321
1325
  continuationKey = result.continuation_key;
1322
1326
  iteration++;
1323
1327
  } while (continuationKey && iteration < maxIterations);
@@ -1342,12 +1346,18 @@ function extractPsuHeaders(req) {
1342
1346
  debug("Using PSU headers for public PSU IP");
1343
1347
  return headers;
1344
1348
  }
1349
+ var ASPSPS_WITHOUT_DATA_ACCESS_PSU_HEADERS = /* @__PURE__ */ new Set(["HypoVereinsbank"]);
1350
+ function dataAccessPsuHeaders(aspspName, psuHeaders) {
1351
+ if (aspspName && ASPSPS_WITHOUT_DATA_ACCESS_PSU_HEADERS.has(aspspName)) return;
1352
+ return psuHeaders;
1353
+ }
1345
1354
  async function buildSessionResult(session, psuHeaders) {
1355
+ const accessHeaders = dataAccessPsuHeaders(session.aspsp?.name, psuHeaders);
1346
1356
  const accountsWithBalances = await Promise.all(session.accounts.map(async (account) => {
1347
1357
  const normalized = normalizeAccount(account, session.aspsp);
1348
1358
  let balances = [];
1349
1359
  try {
1350
- balances = (await enableBankingService.getBalances(account.uid, psuHeaders)).balances.map(normalizeBalance);
1360
+ balances = (await enableBankingService.getBalances(account.uid, accessHeaders)).balances.map(normalizeBalance);
1351
1361
  } catch (err) {
1352
1362
  debug("Failed to fetch balances for account %s: %s", account.uid, err);
1353
1363
  }
@@ -1481,7 +1491,7 @@ app$7.post("/start-auth", handleError(async (req, res) => {
1481
1491
  }
1482
1492
  const state = v4();
1483
1493
  try {
1484
- const authResponse = await enableBankingService.startAuth(aspsp, redirectUrl, state, typeof maxConsentValidity === "number" ? maxConsentValidity : void 0, psuType === "business" ? "business" : "personal");
1494
+ const authResponse = await enableBankingService.startAuth(aspsp, redirectUrl, state, typeof maxConsentValidity === "number" ? maxConsentValidity : void 0, psuType === "business" ? "business" : "personal", extractPsuHeaders(req));
1485
1495
  res.send({
1486
1496
  status: "ok",
1487
1497
  data: {
@@ -1616,7 +1626,7 @@ app$7.post("/poll-auth", handleError(async (req, res) => {
1616
1626
  }
1617
1627
  }));
1618
1628
  app$7.post("/transactions", handleError(async (req, res) => {
1619
- const { accountId, startDate } = req.body || {};
1629
+ const { accountId, startDate, aspspName } = req.body || {};
1620
1630
  if (!accountId || !startDate) {
1621
1631
  res.send({
1622
1632
  status: "ok",
@@ -1627,10 +1637,10 @@ app$7.post("/transactions", handleError(async (req, res) => {
1627
1637
  });
1628
1638
  return;
1629
1639
  }
1630
- const psuHeaders = extractPsuHeaders(req);
1631
1640
  try {
1632
1641
  const dateTo = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1633
1642
  const dateFrom = typeof startDate === "string" ? startDate : new Date(startDate).toISOString().split("T")[0];
1643
+ const psuHeaders = dataAccessPsuHeaders(aspspName, extractPsuHeaders(req));
1634
1644
  const balances = (await enableBankingService.getBalances(accountId, psuHeaders)).balances.map(normalizeBalance);
1635
1645
  let startingBalance = 0;
1636
1646
  if (balances.length > 0) startingBalance = (balances.find((b) => b.balanceType === "CLAV") ?? balances[0]).balanceAmount.amount;
@@ -4996,14 +5006,16 @@ var SyncRequestSchema = messageDesc(file_sync, 3);
4996
5006
  */
4997
5007
  var SyncResponseSchema = messageDesc(file_sync, 4);
4998
5008
  //#endregion
4999
- //#region src/app-sync/errors.js
5009
+ //#region src/app-sync/errors.ts
5000
5010
  var FileNotFound = class extends Error {
5011
+ details;
5001
5012
  constructor(params = {}) {
5002
5013
  super("File does not exist or you don't have access to it");
5003
5014
  this.details = params;
5004
5015
  }
5005
5016
  };
5006
5017
  var GenericFileError = class extends Error {
5018
+ details;
5007
5019
  constructor(message, params = {}) {
5008
5020
  super(message);
5009
5021
  this.details = params;
@@ -5178,10 +5190,10 @@ var FilesService = class {
5178
5190
  }
5179
5191
  };
5180
5192
  //#endregion
5181
- //#region src/app-sync/validation.js
5193
+ //#region src/app-sync/validation.ts
5182
5194
  var SYNC_FORMAT_VERSION = 2;
5183
5195
  var validateSyncedFile = (groupId, keyId, currentFile) => {
5184
- if (currentFile.syncVersion == null || currentFile.syncVersion < SYNC_FORMAT_VERSION) return "file-old-version";
5196
+ if (currentFile.syncVersion == null || Number(currentFile.syncVersion) < SYNC_FORMAT_VERSION) return "file-old-version";
5185
5197
  if (currentFile.groupId == null) return "file-needs-upload";
5186
5198
  if ((currentFile.encryptMeta ? JSON.parse(currentFile.encryptMeta).keyId : null) !== currentFile.encryptKeyId) return "file-key-mismatch";
5187
5199
  if (groupId !== currentFile.groupId) return "file-has-reset";
@@ -5426,14 +5438,18 @@ app$1.post("/upload-user-file", async (req, res) => {
5426
5438
  res.status(400).send("invalid fileId");
5427
5439
  return;
5428
5440
  }
5429
- let groupId = req.headers["x-actual-group-id"] || null;
5441
+ const groupIdHeader = req.headers["x-actual-group-id"];
5430
5442
  const encryptMeta = extractSingleHeader(req, res, "x-actual-encrypt-meta");
5431
5443
  if (res.headersSent) return;
5432
5444
  const syncFormatVersion = extractSingleHeader(req, res, "x-actual-format");
5433
5445
  if (res.headersSent) return;
5434
- if (!!groupId && (typeof groupId !== "string" || !isValidGroupId(groupId))) {
5435
- res.status(400).send("invalid groupId");
5436
- return;
5446
+ let groupId = null;
5447
+ if (groupIdHeader !== void 0) {
5448
+ if (typeof groupIdHeader !== "string" || !isValidGroupId(groupIdHeader)) {
5449
+ res.status(400).send("invalid groupId");
5450
+ return;
5451
+ }
5452
+ groupId = groupIdHeader;
5437
5453
  }
5438
5454
  const keyId = encryptMeta && typeof encryptMeta === "string" ? JSON.parse(encryptMeta).keyId : null;
5439
5455
  const filesService = new FilesService(getAccountDb());
@@ -5750,4 +5766,4 @@ async function run() {
5750
5766
  //#endregion
5751
5767
  export { run };
5752
5768
 
5753
- //# sourceMappingURL=app-BgMEdvse.js.map
5769
+ //# sourceMappingURL=app-C9cIy7Ri.js.map