@actual-app/sync-server 26.7.0 → 26.8.0-nightly.20260704

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.
Files changed (31) hide show
  1. package/build/app.js +2 -2
  2. package/build/bin/actual-server.js +1 -1
  3. package/build/bin/actual-server.js.map +1 -1
  4. package/build/chunks/{1694360479680-create-account-db-CoSya_e5.js → 1694360479680-create-account-db-PwKHxKju.js} +2 -2
  5. package/build/chunks/{1694360479680-create-account-db-CoSya_e5.js.map → 1694360479680-create-account-db-PwKHxKju.js.map} +1 -1
  6. package/build/chunks/{1694362247011-create-secret-table-CQhb5jt5.js → 1694362247011-create-secret-table-CTg8s4SC.js} +2 -2
  7. package/build/chunks/{1694362247011-create-secret-table-CQhb5jt5.js.map → 1694362247011-create-secret-table-CTg8s4SC.js.map} +1 -1
  8. package/build/chunks/{1702667624000-rename-nordigen-secrets-IRFLks8C.js → 1702667624000-rename-nordigen-secrets-BFEve46p.js} +2 -2
  9. package/build/chunks/{1702667624000-rename-nordigen-secrets-IRFLks8C.js.map → 1702667624000-rename-nordigen-secrets-BFEve46p.js.map} +1 -1
  10. package/build/chunks/{1718889148000-openid-Gp-HJB96.js → 1718889148000-openid-9qjigJ2O.js} +2 -2
  11. package/build/chunks/{1718889148000-openid-Gp-HJB96.js.map → 1718889148000-openid-9qjigJ2O.js.map} +1 -1
  12. package/build/chunks/{1719409568000-multiuser-CU5kHE-Z.js → 1719409568000-multiuser-SFJ94reU.js} +2 -2
  13. package/build/chunks/{1719409568000-multiuser-CU5kHE-Z.js.map → 1719409568000-multiuser-SFJ94reU.js.map} +1 -1
  14. package/build/chunks/{1763873568237-server-global-prefs-Bb7sFLeD.js → 1763873568237-server-global-prefs-CZx8n-l2.js} +2 -2
  15. package/build/chunks/{1763873568237-server-global-prefs-Bb7sFLeD.js.map → 1763873568237-server-global-prefs-CZx8n-l2.js.map} +1 -1
  16. package/build/chunks/{1763873600000-backfill-files-owner-DuVG9XcJ.js → 1763873600000-backfill-files-owner-B8HCP881.js} +2 -2
  17. package/build/chunks/{1763873600000-backfill-files-owner-DuVG9XcJ.js.map → 1763873600000-backfill-files-owner-B8HCP881.js.map} +1 -1
  18. package/build/chunks/{account-db-HkRNud0e.js → account-db-B0u6vCZk.js} +51 -24
  19. package/build/chunks/account-db-B0u6vCZk.js.map +1 -0
  20. package/build/chunks/{app-CLRlkXbY.js → app-DMyQvS9q.js} +170 -51
  21. package/build/chunks/app-DMyQvS9q.js.map +1 -0
  22. package/build/chunks/{migrations-Cneard4X.js → migrations-DOEWw8Fg.js} +8 -8
  23. package/build/chunks/{migrations-Cneard4X.js.map → migrations-DOEWw8Fg.js.map} +1 -1
  24. package/build/scripts/disable-openid.js +1 -1
  25. package/build/scripts/enable-openid.js +1 -1
  26. package/build/scripts/reset-password.js +2 -2
  27. package/build/scripts/reset-password.js.map +1 -1
  28. package/build/scripts/run-migrations.js +1 -1
  29. package/package.json +3 -2
  30. package/build/chunks/account-db-HkRNud0e.js.map +0 -1
  31. package/build/chunks/app-CLRlkXbY.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { t as configSchema } from "./load-config-8NvK3qSh.js";
2
- import { A as getFileById, B as updateUserWithRole, C as checkFilePermission, D as deleteUserAccessByFileId, E as deleteUserAccess, F as getUserById, I as getUserByUsername, L as insertUser, M as getOwnerCount, N as getOwnerId, O as getAllUserAccess, P as getUserAccess, R as transferAllFilesFromUser, S as addUserAccess, T as deleteUser, V as validateRole, _ as changePassword, a as getActiveLoginMethod, b as validateAuthHeader, c as getUserInfo, d as needsBootstrap, f as setServerPrefs, g as loginWithOpenIdSetup, h as loginWithOpenIdFinalize, i as getAccountDb, j as getOpenIDConfig, k as getAllUsers, l as isAdmin, m as isValidRedirectUrl, n as disableOpenID, o as getLoginMethod, p as openDatabase, r as enableOpenID, s as getServerPrefs, t as bootstrap, u as listLoginMethods, v as checkPassword, w as countUserAccess, x as validateSession, y as loginWithPassword, z as updateFileOwner } from "./account-db-HkRNud0e.js";
2
+ import { A as getFileById, B as updateUserWithRole, C as checkFilePermission, D as deleteUserAccessByFileId, E as deleteUserAccess, F as getUserById, I as getUserByUsername, L as insertUser, M as getOwnerCount, N as getOwnerId, O as getAllUserAccess, P as getUserAccess, R as transferAllFilesFromUser, S as addUserAccess, T as deleteUser, V as validateRole, _ as changePassword, a as getActiveLoginMethod, b as validateAuthHeader, c as getUserInfo, d as needsBootstrap, f as setServerPrefs, g as loginWithOpenIdSetup, h as loginWithOpenIdFinalize, i as getAccountDb, j as getOpenIDConfig, k as getAllUsers, l as isAdmin, m as isValidRedirectUrl, n as disableOpenID, o as getLoginMethod, p as openDatabase, r as enableOpenID, s as getServerPrefs, t as bootstrap, u as listLoginMethods, v as checkPassword, w as countUserAccess, x as validateSession, y as loginWithPassword, z as updateFileOwner } from "./account-db-B0u6vCZk.js";
3
3
  import { join, resolve } from "node:path";
4
4
  import fs, { existsSync, readFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
@@ -128,7 +128,7 @@ app$11.post("/login", authRateLimiter, async (req, res) => {
128
128
  reason: "invalid-header"
129
129
  });
130
130
  return;
131
- } else if (validateAuthHeader(req)) tokenRes = loginWithPassword(headerVal);
131
+ } else if (validateAuthHeader(req)) tokenRes = await loginWithPassword(headerVal);
132
132
  else {
133
133
  res.send({
134
134
  status: "error",
@@ -161,7 +161,7 @@ app$11.post("/login", authRateLimiter, async (req, res) => {
161
161
  return;
162
162
  }
163
163
  default:
164
- tokenRes = loginWithPassword(req.body.password);
164
+ tokenRes = await loginWithPassword(req.body.password);
165
165
  break;
166
166
  }
167
167
  const { error, token } = tokenRes;
@@ -177,7 +177,7 @@ app$11.post("/login", authRateLimiter, async (req, res) => {
177
177
  data: { token }
178
178
  });
179
179
  });
180
- app$11.post("/change-password", (req, res) => {
180
+ app$11.post("/change-password", async (req, res) => {
181
181
  const session = validateSession(req, res);
182
182
  if (!session) return;
183
183
  if (!isAdmin(session.user_id)) {
@@ -196,7 +196,7 @@ app$11.post("/change-password", (req, res) => {
196
196
  });
197
197
  return;
198
198
  }
199
- const { error } = changePassword(req.body.password);
199
+ const { error } = await changePassword(req.body.password);
200
200
  if (error) {
201
201
  res.status(400).send({
202
202
  status: "error",
@@ -590,6 +590,9 @@ var SecretName = {
590
590
  enablebanking_applicationId: "enablebanking_applicationId",
591
591
  enablebanking_secretKey: "enablebanking_secretKey"
592
592
  };
593
+ function getSecretKey(name, fileId) {
594
+ return fileId == null ? name : `${name}:${fileId}`;
595
+ }
593
596
  var SecretsDb = class {
594
597
  constructor() {
595
598
  this.debug = createDebug("actual:secrets-db");
@@ -598,49 +601,62 @@ var SecretsDb = class {
598
601
  open() {
599
602
  return getAccountDb();
600
603
  }
601
- set(name, value) {
604
+ set(name, value, fileId = null) {
602
605
  if (!this.db) this.db = this.open();
606
+ const secretKey = getSecretKey(name, fileId);
603
607
  this.debug(`setting secret '${name}' to '${value}'`);
604
- return this.db.mutate(`INSERT OR REPLACE INTO secrets (name, value) VALUES (?,?)`, [name, value]);
608
+ return this.db.mutate(`INSERT OR REPLACE INTO secrets (name, value) VALUES (?, ?)`, [secretKey, value]);
605
609
  }
606
- get(name) {
610
+ get(name, fileId = null) {
607
611
  if (!this.db) this.db = this.open();
612
+ const secretKey = getSecretKey(name, fileId);
608
613
  this.debug(`getting secret '${name}'`);
609
- return this.db.first(`SELECT value FROM secrets WHERE name =?`, [name]);
614
+ return this.db.first(`SELECT value FROM secrets WHERE name = ?`, [secretKey]);
615
+ }
616
+ reset(name, fileId = null) {
617
+ if (!this.db) this.db = this.open();
618
+ const secretKey = getSecretKey(name, fileId);
619
+ return {
620
+ ...this.db.mutate(`DELETE FROM secrets WHERE name = ?`, [secretKey]),
621
+ deletedFrom: fileId == null ? "global" : "per-budget-file"
622
+ };
610
623
  }
611
624
  };
612
625
  var secretsDb = new SecretsDb();
613
- var _cachedSecrets = /* @__PURE__ */ new Map();
614
626
  /**
615
627
  * A service for managing secrets stored in `secretsDb`.
616
628
  */
617
629
  var secretsService = {
618
630
  /**
619
- * Retrieves the value of a secret by name.
631
+ * Retrieves the active value of a secret by name.
620
632
  * @param {SecretName} name - The name of the secret to retrieve.
633
+ * @param {string=} fileId - Budget file ID for this secret.
621
634
  * @returns {string|null} The value of the secret, or null if the secret does not exist.
622
635
  */
623
- get: (name) => {
624
- return _cachedSecrets.get(name) ?? secretsDb.get(name)?.value ?? null;
636
+ get: (name, fileId = null) => {
637
+ return secretsDb.get(name, fileId)?.value ?? null;
625
638
  },
626
639
  /**
627
640
  * Sets the value of a secret by name.
628
641
  * @param {SecretName} name - The name of the secret to set.
629
642
  * @param {string} value - The value to set for the secret.
643
+ * @param {string=} fileId - Budget file ID for this secret.
630
644
  * @returns {Object}
631
645
  */
632
- set: (name, value) => {
633
- const result = secretsDb.set(name, value);
634
- if (result.changes === 1) _cachedSecrets.set(name, value);
635
- return result;
646
+ set: (name, value, fileId = null) => {
647
+ return secretsDb.set(name, value, fileId);
648
+ },
649
+ reset: (name, fileId = null) => {
650
+ return secretsDb.reset(name, fileId);
636
651
  },
637
652
  /**
638
653
  * Determines whether a secret with the given name exists.
639
654
  * @param {SecretName} name - The name of the secret to check for existence.
655
+ * @param {string=} fileId - Budget file ID for this secret.
640
656
  * @returns {boolean} True if a secret with the given name exists, false otherwise.
641
657
  */
642
- exists: (name) => {
643
- return Boolean(secretsService.get(name));
658
+ exists: (name, fileId = null) => {
659
+ return secretsService.get(name, fileId) !== null;
644
660
  }
645
661
  };
646
662
  //#endregion
@@ -3854,7 +3870,7 @@ app$5.post("/config", openIdConfigRateLimiter, async (req, res) => {
3854
3870
  });
3855
3871
  return;
3856
3872
  }
3857
- if (!checkPassword(req.body.password)) {
3873
+ if (!await checkPassword(req.body.password)) {
3858
3874
  res.status(400).send({
3859
3875
  status: "error",
3860
3876
  reason: "invalid-password"
@@ -4035,6 +4051,8 @@ app$4.post("/transactions", handleError(async (req, res) => {
4035
4051
  };
4036
4052
  newTrans.transactionId = trans.id;
4037
4053
  newTrans.sortOrder = transactionDate.getTime();
4054
+ newTrans.originalDate = getDate$1(transactionDate);
4055
+ newTrans.date = getDate$1(getTransactionDateCorrected(trans));
4038
4056
  delete trans.amount;
4039
4057
  const finalTrans = {
4040
4058
  ...flattenObject(trans),
@@ -4089,51 +4107,167 @@ function getPayeeName(trans) {
4089
4107
  }
4090
4108
  return "";
4091
4109
  }
4110
+ function addMonthsClamped(date, months) {
4111
+ const result = new Date(date);
4112
+ const day = result.getUTCDate();
4113
+ result.setUTCDate(1);
4114
+ result.setUTCMonth(result.getUTCMonth() + months);
4115
+ const lastDay = new Date(Date.UTC(result.getUTCFullYear(), result.getUTCMonth() + 1, 0)).getUTCDate();
4116
+ result.setUTCDate(Math.min(day, lastDay));
4117
+ return result;
4118
+ }
4119
+ function getTransactionDateCorrected(trans) {
4120
+ if (trans.creditCardMetadata?.installmentNumber != null) return addMonthsClamped(trans.creditCardMetadata.purchaseDate || trans.date, trans.creditCardMetadata.installmentNumber - 1);
4121
+ return trans.date;
4122
+ }
4123
+ //#endregion
4124
+ //#region src/util/paths.ts
4125
+ var ID_REGEX = /^[a-zA-Z0-9_-]+$/;
4126
+ function isValidFileId(id) {
4127
+ return ID_REGEX.test(id);
4128
+ }
4129
+ function isValidGroupId(id) {
4130
+ return ID_REGEX.test(id);
4131
+ }
4132
+ function getPathForUserFile(fileId) {
4133
+ return join(resolve(configSchema.get("userFiles")), `file-${fileId}.blob`);
4134
+ }
4135
+ function getPathForGroupFile(groupId) {
4136
+ return join(resolve(configSchema.get("userFiles")), `group-${groupId}.sqlite`);
4137
+ }
4092
4138
  //#endregion
4093
4139
  //#region src/app-secrets.js
4094
4140
  var app$3 = express();
4095
4141
  app$3.use(express.json());
4096
4142
  app$3.use(requestLoggerMiddleware);
4097
4143
  app$3.use(validateSessionMiddleware);
4098
- function canManageSecrets(userId) {
4099
- return getActiveLoginMethod() !== "openid" || isAdmin(userId);
4144
+ function canManageGlobalSecrets(userId) {
4145
+ return isAdmin(userId);
4146
+ }
4147
+ function isBudgetFileOwner(fileId, userId) {
4148
+ const { granted } = checkFilePermission(fileId, userId) || { granted: 0 };
4149
+ return granted > 0;
4150
+ }
4151
+ function canManagePerBudgetFileSecrets(fileId, userId) {
4152
+ return isAdmin(userId) || isBudgetFileOwner(fileId, userId);
4100
4153
  }
4101
4154
  app$3.post("/", async (req, res) => {
4102
- if (!canManageSecrets(res.locals.user_id)) {
4155
+ const { name, value } = req.body || {};
4156
+ const fileId = req.get("X-Actual-File-Id");
4157
+ const perBudgetFile = fileId != null;
4158
+ if (!(name in SecretName)) {
4159
+ res.status(400).send({
4160
+ status: "error",
4161
+ reason: "invalid-secret-name",
4162
+ details: "Unknown secret name"
4163
+ });
4164
+ return;
4165
+ }
4166
+ if (perBudgetFile) {
4167
+ if (!isValidFileId(fileId)) {
4168
+ res.status(400).send({
4169
+ status: "error",
4170
+ reason: "invalid-file-id",
4171
+ details: "invalid fileId"
4172
+ });
4173
+ return;
4174
+ }
4175
+ if (!canManagePerBudgetFileSecrets(fileId, res.locals.user_id)) {
4176
+ res.status(403).send({
4177
+ status: "error",
4178
+ reason: "file-access-denied",
4179
+ details: "You don't have permissions over this file"
4180
+ });
4181
+ return;
4182
+ }
4183
+ } else if (!canManageGlobalSecrets(res.locals.user_id)) {
4103
4184
  res.status(403).send({
4104
4185
  status: "error",
4105
4186
  reason: "not-admin",
4106
- details: "You have to be admin to set secrets"
4187
+ details: "You have to be admin to manage global secrets"
4107
4188
  });
4108
4189
  return;
4109
4190
  }
4110
- const { name, value } = req.body || {};
4191
+ const secretFileId = perBudgetFile ? fileId : null;
4192
+ secretsService.set(name, value, secretFileId);
4193
+ res.status(200).send({ status: "ok" });
4194
+ });
4195
+ app$3.delete("/:name", async (req, res) => {
4196
+ const name = req.params.name;
4197
+ const fileId = req.get("X-Actual-File-Id");
4198
+ const perBudgetFile = fileId != null;
4111
4199
  if (!(name in SecretName)) {
4200
+ res.status(404).send("key not found");
4201
+ return;
4202
+ }
4203
+ if (!perBudgetFile) {
4204
+ if (!canManageGlobalSecrets(res.locals.user_id)) {
4205
+ res.status(403).send({
4206
+ status: "error",
4207
+ reason: "not-admin",
4208
+ details: "You have to be admin to manage global secrets"
4209
+ });
4210
+ return;
4211
+ }
4212
+ secretsService.reset(name);
4213
+ res.status(200).send({ status: "ok" });
4214
+ return;
4215
+ }
4216
+ if (!isValidFileId(fileId)) {
4112
4217
  res.status(400).send({
4113
4218
  status: "error",
4114
- reason: "invalid-secret-name",
4115
- details: "Unknown secret name"
4219
+ reason: "invalid-file-id",
4220
+ details: "invalid fileId"
4116
4221
  });
4117
4222
  return;
4118
4223
  }
4119
- secretsService.set(name, value);
4120
- res.status(200).send({ status: "ok" });
4121
- });
4122
- app$3.get("/:name", async (req, res) => {
4123
- if (!canManageSecrets(res.locals.user_id)) {
4224
+ if (!canManagePerBudgetFileSecrets(fileId, res.locals.user_id)) {
4124
4225
  res.status(403).send({
4125
4226
  status: "error",
4126
- reason: "not-admin",
4127
- details: "You have to be admin to read secrets"
4227
+ reason: "file-access-denied",
4228
+ details: "You don't have permissions over this file"
4128
4229
  });
4129
4230
  return;
4130
4231
  }
4232
+ secretsService.reset(name, fileId);
4233
+ res.status(200).send({ status: "ok" });
4234
+ });
4235
+ app$3.get("/:name", async (req, res) => {
4131
4236
  const name = req.params.name;
4237
+ const fileId = req.get("X-Actual-File-Id");
4238
+ const perBudgetFile = fileId != null;
4132
4239
  if (!(name in SecretName)) {
4133
4240
  res.status(404).send("key not found");
4134
4241
  return;
4135
4242
  }
4136
- if (secretsService.exists(name)) res.sendStatus(204);
4243
+ if (!perBudgetFile) {
4244
+ if (!canManageGlobalSecrets(res.locals.user_id)) {
4245
+ res.status(403).send({
4246
+ status: "error",
4247
+ reason: "not-admin",
4248
+ details: "You have to be admin to manage global secrets"
4249
+ });
4250
+ return;
4251
+ }
4252
+ } else {
4253
+ if (!isValidFileId(fileId)) {
4254
+ res.status(400).send({
4255
+ status: "error",
4256
+ reason: "invalid-file-id",
4257
+ details: "invalid fileId"
4258
+ });
4259
+ return;
4260
+ }
4261
+ if (!canManagePerBudgetFileSecrets(fileId, res.locals.user_id)) {
4262
+ res.status(403).send({
4263
+ status: "error",
4264
+ reason: "file-access-denied",
4265
+ details: "You don't have permissions over this file"
4266
+ });
4267
+ return;
4268
+ }
4269
+ }
4270
+ if (secretsService.exists(name, perBudgetFile ? fileId : null)) res.sendStatus(204);
4137
4271
  else res.status(404).send("key not found");
4138
4272
  });
4139
4273
  //#endregion
@@ -4660,21 +4794,6 @@ var GenericFileError = class extends Error {
4660
4794
  }
4661
4795
  };
4662
4796
  //#endregion
4663
- //#region src/util/paths.ts
4664
- var ID_REGEX = /^[a-zA-Z0-9_-]+$/;
4665
- function isValidFileId(id) {
4666
- return ID_REGEX.test(id);
4667
- }
4668
- function isValidGroupId(id) {
4669
- return ID_REGEX.test(id);
4670
- }
4671
- function getPathForUserFile(fileId) {
4672
- return join(resolve(configSchema.get("userFiles")), `file-${fileId}.blob`);
4673
- }
4674
- function getPathForGroupFile(groupId) {
4675
- return join(resolve(configSchema.get("userFiles")), `group-${groupId}.sqlite`);
4676
- }
4677
- //#endregion
4678
4797
  //#region src/app-sync/services/files-service.ts
4679
4798
  var FileBase = class {
4680
4799
  name;
@@ -5415,4 +5534,4 @@ async function run() {
5415
5534
  //#endregion
5416
5535
  export { run };
5417
5536
 
5418
- //# sourceMappingURL=app-CLRlkXbY.js.map
5537
+ //# sourceMappingURL=app-DMyQvS9q.js.map