@technomoron/api-server-base 2.0.0-beta.17 → 2.0.0-beta.19

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 (63) hide show
  1. package/README.txt +48 -35
  2. package/dist/cjs/api-module.cjs +9 -0
  3. package/dist/cjs/api-module.d.ts +4 -2
  4. package/dist/cjs/api-server-base.cjs +178 -57
  5. package/dist/cjs/api-server-base.d.ts +31 -2
  6. package/dist/cjs/auth-api/auth-module.d.ts +12 -1
  7. package/dist/cjs/auth-api/auth-module.js +77 -35
  8. package/dist/cjs/auth-api/mem-auth-store.js +2 -23
  9. package/dist/cjs/auth-api/sql-auth-store.js +4 -31
  10. package/dist/cjs/auth-api/user-id.d.ts +4 -0
  11. package/dist/cjs/auth-api/user-id.js +31 -0
  12. package/dist/cjs/auth-cookie-options.d.ts +11 -0
  13. package/dist/cjs/auth-cookie-options.js +57 -0
  14. package/dist/cjs/oauth/memory.js +4 -10
  15. package/dist/cjs/oauth/models.js +4 -15
  16. package/dist/cjs/oauth/sequelize.js +8 -23
  17. package/dist/cjs/passkey/config.d.ts +2 -0
  18. package/dist/cjs/passkey/config.js +26 -0
  19. package/dist/cjs/passkey/memory.js +2 -9
  20. package/dist/cjs/passkey/models.js +4 -15
  21. package/dist/cjs/passkey/sequelize.js +6 -22
  22. package/dist/cjs/passkey/service.js +1 -1
  23. package/dist/cjs/passkey/types.d.ts +5 -0
  24. package/dist/cjs/sequelize-utils.d.ts +3 -0
  25. package/dist/cjs/sequelize-utils.js +17 -0
  26. package/dist/cjs/token/memory.d.ts +4 -0
  27. package/dist/cjs/token/memory.js +90 -25
  28. package/dist/cjs/token/sequelize.js +16 -22
  29. package/dist/cjs/token/types.d.ts +7 -0
  30. package/dist/cjs/user/memory.js +2 -9
  31. package/dist/cjs/user/sequelize.js +6 -22
  32. package/dist/esm/api-module.d.ts +4 -2
  33. package/dist/esm/api-module.js +9 -0
  34. package/dist/esm/api-server-base.d.ts +31 -2
  35. package/dist/esm/api-server-base.js +178 -57
  36. package/dist/esm/auth-api/auth-module.d.ts +12 -1
  37. package/dist/esm/auth-api/auth-module.js +77 -35
  38. package/dist/esm/auth-api/mem-auth-store.js +1 -22
  39. package/dist/esm/auth-api/sql-auth-store.js +2 -29
  40. package/dist/esm/auth-api/user-id.d.ts +4 -0
  41. package/dist/esm/auth-api/user-id.js +26 -0
  42. package/dist/esm/auth-cookie-options.d.ts +11 -0
  43. package/dist/esm/auth-cookie-options.js +54 -0
  44. package/dist/esm/oauth/memory.js +4 -10
  45. package/dist/esm/oauth/models.js +1 -12
  46. package/dist/esm/oauth/sequelize.js +5 -20
  47. package/dist/esm/passkey/config.d.ts +2 -0
  48. package/dist/esm/passkey/config.js +23 -0
  49. package/dist/esm/passkey/memory.js +2 -9
  50. package/dist/esm/passkey/models.js +1 -12
  51. package/dist/esm/passkey/sequelize.js +3 -19
  52. package/dist/esm/passkey/service.js +1 -1
  53. package/dist/esm/passkey/types.d.ts +5 -0
  54. package/dist/esm/sequelize-utils.d.ts +3 -0
  55. package/dist/esm/sequelize-utils.js +12 -0
  56. package/dist/esm/token/memory.d.ts +4 -0
  57. package/dist/esm/token/memory.js +90 -25
  58. package/dist/esm/token/sequelize.js +12 -18
  59. package/dist/esm/token/types.d.ts +7 -0
  60. package/dist/esm/user/memory.js +2 -9
  61. package/dist/esm/user/sequelize.js +3 -19
  62. package/docs/swagger/openapi.json +11 -145
  63. package/package.json +12 -12
@@ -6,12 +6,12 @@ function comparableUserId(value) {
6
6
  return String(value);
7
7
  }
8
8
  function cloneToken(record) {
9
- // this.normalizeToken is not available in static context; caller passes through instance.
10
- // cloning handled via store instance methods.
11
- const normalized = record;
12
9
  return {
13
- ...normalized,
14
- scope: normalized.scope ? [...normalized.scope] : undefined
10
+ ...record,
11
+ scope: record.scope ? [...record.scope] : undefined,
12
+ expires: record.expires ? new Date(record.expires) : undefined,
13
+ issuedAt: record.issuedAt ? new Date(record.issuedAt) : undefined,
14
+ lastSeenAt: record.lastSeenAt ? new Date(record.lastSeenAt) : undefined
15
15
  };
16
16
  }
17
17
  function matchesQuery(record, query, includeExpired) {
@@ -47,15 +47,57 @@ function matchesQuery(record, query, includeExpired) {
47
47
  export class MemoryTokenStore extends TokenStore {
48
48
  constructor() {
49
49
  super(...arguments);
50
- this.tokens = [];
50
+ this.tokens = new Map();
51
+ this.tokensByUser = new Map();
52
+ }
53
+ indexToken(token) {
54
+ const userId = comparableUserId(token.userId);
55
+ if (!userId) {
56
+ return;
57
+ }
58
+ let userTokens = this.tokensByUser.get(userId);
59
+ if (!userTokens) {
60
+ userTokens = new Set();
61
+ this.tokensByUser.set(userId, userTokens);
62
+ }
63
+ userTokens.add(token.refreshToken);
64
+ }
65
+ unindexToken(token) {
66
+ const userId = comparableUserId(token.userId);
67
+ if (!userId) {
68
+ return;
69
+ }
70
+ const userTokens = this.tokensByUser.get(userId);
71
+ if (!userTokens) {
72
+ return;
73
+ }
74
+ userTokens.delete(token.refreshToken);
75
+ if (userTokens.size === 0) {
76
+ this.tokensByUser.delete(userId);
77
+ }
78
+ }
79
+ removeByRefreshToken(refreshToken) {
80
+ const existing = this.tokens.get(refreshToken);
81
+ if (!existing) {
82
+ return;
83
+ }
84
+ this.unindexToken(existing);
85
+ this.tokens.delete(refreshToken);
51
86
  }
52
87
  async save(record) {
53
88
  const stored = this.normalizeToken(record);
54
89
  const normalizedUserId = comparableUserId(stored.userId);
90
+ if (!normalizedUserId) {
91
+ throw new Error('userId is required');
92
+ }
55
93
  const domainProvided = record.domain !== undefined;
56
94
  const fingerprintProvided = record.fingerprint !== undefined;
57
- for (let index = this.tokens.length - 1; index >= 0; index -= 1) {
58
- const existing = this.tokens[index];
95
+ const userRefreshTokens = [...(this.tokensByUser.get(normalizedUserId) ?? [])];
96
+ for (const refreshToken of userRefreshTokens) {
97
+ const existing = this.tokens.get(refreshToken);
98
+ if (!existing) {
99
+ continue;
100
+ }
59
101
  if (comparableUserId(existing.userId) !== normalizedUserId) {
60
102
  continue;
61
103
  }
@@ -68,44 +110,51 @@ export class MemoryTokenStore extends TokenStore {
68
110
  if (fingerprintProvided && existing.fingerprint !== stored.fingerprint) {
69
111
  continue;
70
112
  }
71
- this.tokens.splice(index, 1);
113
+ this.removeByRefreshToken(existing.refreshToken);
72
114
  }
73
- this.tokens.push(stored);
115
+ this.removeByRefreshToken(stored.refreshToken);
116
+ this.tokens.set(stored.refreshToken, stored);
117
+ this.indexToken(stored);
74
118
  }
75
119
  async get(query, opts) {
76
120
  if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
77
121
  throw new Error('At least one token lookup field must be provided');
78
122
  }
79
123
  const includeExpired = opts?.includeExpired ?? false;
80
- const record = this.tokens.find((token) => matchesQuery(token, query, includeExpired));
81
- return record ? cloneToken(record) : null;
124
+ if (query.refreshToken) {
125
+ const record = this.tokens.get(query.refreshToken);
126
+ return record && matchesQuery(record, query, includeExpired) ? cloneToken(record) : null;
127
+ }
128
+ for (const token of this.tokens.values()) {
129
+ if (matchesQuery(token, query, includeExpired)) {
130
+ return cloneToken(token);
131
+ }
132
+ }
133
+ return null;
82
134
  }
83
135
  async delete(query) {
84
136
  if (!query.refreshToken && !query.accessToken && query.userId === undefined && !query.clientId) {
85
137
  return 0;
86
138
  }
87
139
  let removed = 0;
88
- for (let index = this.tokens.length - 1; index >= 0; index -= 1) {
89
- if (matchesQuery(this.tokens[index], query, true)) {
90
- this.tokens.splice(index, 1);
140
+ const refreshTokens = [...this.tokens.keys()];
141
+ for (const refreshToken of refreshTokens) {
142
+ const token = this.tokens.get(refreshToken);
143
+ if (token && matchesQuery(token, query, true)) {
144
+ this.removeByRefreshToken(refreshToken);
91
145
  removed += 1;
92
146
  }
93
147
  }
94
148
  return removed;
95
149
  }
96
150
  async update(params) {
97
- const token = this.tokens.find((record) => {
98
- if (record.refreshToken !== params.refreshToken) {
99
- return false;
100
- }
101
- if (params.clientId && record.clientId !== params.clientId) {
102
- return false;
103
- }
104
- return true;
105
- });
151
+ const token = this.tokens.get(params.refreshToken);
106
152
  if (!token) {
107
153
  return false;
108
154
  }
155
+ if (params.clientId && token.clientId !== params.clientId) {
156
+ return false;
157
+ }
109
158
  const merged = { ...token };
110
159
  const maybeAssign = (key) => {
111
160
  const value = params[key];
@@ -129,12 +178,28 @@ export class MemoryTokenStore extends TokenStore {
129
178
  maybeAssign('lastSeenAt');
130
179
  maybeAssign('sessionCookie');
131
180
  const normalized = this.normalizeToken(merged);
181
+ const previousUserId = token.userId;
182
+ const previousRefreshToken = token.refreshToken;
183
+ const userChanged = comparableUserId(previousUserId) !== comparableUserId(normalized.userId);
132
184
  Object.assign(token, normalized);
185
+ if (userChanged || previousRefreshToken !== token.refreshToken) {
186
+ this.unindexToken({ ...token, userId: previousUserId, refreshToken: previousRefreshToken });
187
+ this.indexToken(token);
188
+ if (previousRefreshToken !== token.refreshToken) {
189
+ this.tokens.delete(previousRefreshToken);
190
+ this.tokens.set(token.refreshToken, token);
191
+ }
192
+ }
133
193
  return true;
134
194
  }
135
195
  async list(userId, opts = {}) {
136
196
  const includeExpired = opts.includeExpired ?? false;
137
- const filtered = this.tokens.filter((token) => matchesQuery(token, { userId: comparableUserId(userId) }, includeExpired));
197
+ const normalizedUserId = comparableUserId(userId);
198
+ const userRefreshTokens = normalizedUserId ? [...(this.tokensByUser.get(normalizedUserId) ?? [])] : [];
199
+ const filtered = userRefreshTokens
200
+ .map((refreshToken) => this.tokens.get(refreshToken))
201
+ .filter((token) => Boolean(token))
202
+ .filter((token) => matchesQuery(token, { userId: normalizedUserId }, includeExpired));
138
203
  const offset = opts.offset ?? 0;
139
204
  const limit = opts.limit ?? filtered.length;
140
205
  return filtered.slice(offset, offset + limit).map(cloneToken);
@@ -1,17 +1,7 @@
1
1
  import { DataTypes, Model, Op } from 'sequelize';
2
+ import { normalizeStringUserId } from '../auth-api/user-id.js';
3
+ import { DIALECTS_SUPPORTING_UNSIGNED, applyTablePrefix } from '../sequelize-utils.js';
2
4
  import { TokenStore } from './base.js';
3
- const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
4
- function normalizeTablePrefix(prefix) {
5
- if (!prefix) {
6
- return undefined;
7
- }
8
- const trimmed = prefix.trim();
9
- return trimmed.length > 0 ? trimmed : undefined;
10
- }
11
- function applyTablePrefix(prefix, tableName) {
12
- const normalized = normalizeTablePrefix(prefix);
13
- return normalized ? `${normalized}${tableName}` : tableName;
14
- }
15
5
  class TokenModel extends Model {
16
6
  }
17
7
  function tokenTableOptions(sequelize, tablePrefix) {
@@ -64,11 +54,11 @@ function initTokenModel(sequelize, options = {}) {
64
54
  defaultValue: DataTypes.NOW
65
55
  },
66
56
  access: {
67
- type: DataTypes.STRING(512),
57
+ type: DataTypes.STRING(768),
68
58
  allowNull: false
69
59
  },
70
60
  refresh: {
71
- type: DataTypes.STRING(512),
61
+ type: DataTypes.STRING(768),
72
62
  allowNull: false
73
63
  },
74
64
  domain: {
@@ -181,6 +171,13 @@ export class SequelizeTokenStore extends TokenStore {
181
171
  removalWhere.client_id = normalized.clientId;
182
172
  }
183
173
  await this.Tokens.destroy({ where: removalWhere });
174
+ // Access/refresh columns are unique. Remove stale collisions before insert to avoid
175
+ // transient uniqueness failures during retries/rotation edge-cases.
176
+ await this.Tokens.destroy({
177
+ where: {
178
+ [Op.or]: [{ access: normalized.accessToken ?? '' }, { refresh: normalized.refreshToken }]
179
+ }
180
+ });
184
181
  await this.Tokens.create({
185
182
  user_id: resolvedUserId,
186
183
  real_user_id: resolvedRealUserId,
@@ -340,10 +337,7 @@ export class SequelizeTokenStore extends TokenStore {
340
337
  return;
341
338
  }
342
339
  normalizeUserId(identifier) {
343
- if (identifier === undefined || identifier === null) {
344
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
345
- }
346
- return String(identifier);
340
+ return normalizeStringUserId(identifier);
347
341
  }
348
342
  resolveRealUserId(ruid) {
349
343
  if (ruid === undefined || ruid === null) {
@@ -9,7 +9,14 @@ export interface Token {
9
9
  status?: TokenStatus;
10
10
  ruid?: string;
11
11
  clientId?: string;
12
+ /**
13
+ * Optional session partition key. Token stores may use `domain` and `fingerprint`
14
+ * to replace previous sessions that match the same bucket.
15
+ */
12
16
  domain?: string;
17
+ /**
18
+ * Optional device/session fingerprint used together with `domain` for session bucketing.
19
+ */
13
20
  fingerprint?: string;
14
21
  label?: string;
15
22
  browser?: string;
@@ -1,16 +1,9 @@
1
+ import { normalizeNumericUserId } from '../auth-api/user-id.js';
1
2
  import { UserStore } from './base.js';
2
3
  function cloneUser(user) {
3
4
  return { ...user };
4
5
  }
5
- function normalizeUserId(identifier) {
6
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
7
- return identifier;
8
- }
9
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
10
- return Number(identifier);
11
- }
12
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
13
- }
6
+ const normalizeUserId = normalizeNumericUserId;
14
7
  export class MemoryUserStore extends UserStore {
15
8
  constructor(options = {}) {
16
9
  super({
@@ -1,17 +1,7 @@
1
1
  import { DataTypes, Model, Op } from 'sequelize';
2
+ import { normalizeNumericUserId } from '../auth-api/user-id.js';
3
+ import { DIALECTS_SUPPORTING_UNSIGNED, applyTablePrefix } from '../sequelize-utils.js';
2
4
  import { UserStore } from './base.js';
3
- const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
4
- function normalizeTablePrefix(prefix) {
5
- if (!prefix) {
6
- return undefined;
7
- }
8
- const trimmed = prefix.trim();
9
- return trimmed.length > 0 ? trimmed : undefined;
10
- }
11
- function applyTablePrefix(prefix, tableName) {
12
- const normalized = normalizeTablePrefix(prefix);
13
- return normalized ? `${normalized}${tableName}` : tableName;
14
- }
15
5
  function integerIdType(sequelize) {
16
6
  return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
17
7
  }
@@ -178,12 +168,6 @@ export class SequelizeUserStore extends UserStore {
178
168
  };
179
169
  }
180
170
  normalizeUserId(identifier) {
181
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
182
- return identifier;
183
- }
184
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
185
- return Number(identifier);
186
- }
187
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
171
+ return normalizeNumericUserId(identifier);
188
172
  }
189
173
  }
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "API Server Base",
5
- "version": "2.0.0-beta.17",
5
+ "version": "2.0.0-beta.19",
6
6
  "description": "OpenAPI reference for ApiServer base endpoints and optional modules. Auth, passkey, and oauth modules are optional and require the corresponding module to be enabled in the ApiServer config. Base endpoints are always available."
7
7
  },
8
8
  "servers": [
@@ -34,7 +34,7 @@
34
34
  "get": {
35
35
  "tags": ["base"],
36
36
  "summary": "Health check",
37
- "description": "Auth: none. Returns server health and version metadata.",
37
+ "description": "Auth: none. Returns ApiResponse<PingResponseData> with server health and version metadata.",
38
38
  "security": [],
39
39
  "responses": {
40
40
  "200": {
@@ -760,145 +760,6 @@
760
760
  }
761
761
  }
762
762
  }
763
- },
764
- "delete": {
765
- "tags": ["passkey"],
766
- "summary": "Delete a passkey credential",
767
- "description": "Auth: strict. Deletes a passkey credential by id provided in the request body.",
768
- "security": [{ "accessTokenBearer": [] }, { "accessTokenCookie": [] }, { "apiKeyBearer": [] }],
769
- "requestBody": {
770
- "required": true,
771
- "content": {
772
- "application/json": {
773
- "schema": {
774
- "type": "object",
775
- "properties": {
776
- "credentialId": {
777
- "type": "string",
778
- "description": "Credential id (base64url)."
779
- }
780
- },
781
- "required": ["credentialId"]
782
- }
783
- }
784
- }
785
- },
786
- "responses": {
787
- "200": {
788
- "description": "Credential deleted.",
789
- "content": {
790
- "application/json": {
791
- "schema": {
792
- "allOf": [
793
- { "$ref": "#/components/schemas/ApiResponse" },
794
- {
795
- "type": "object",
796
- "properties": {
797
- "data": { "$ref": "#/components/schemas/PasskeyDeleteResponseData" }
798
- },
799
- "required": ["data"]
800
- }
801
- ]
802
- }
803
- }
804
- }
805
- },
806
- "400": {
807
- "description": "Missing or invalid credentialId.",
808
- "content": {
809
- "application/json": {
810
- "schema": {
811
- "allOf": [
812
- { "$ref": "#/components/schemas/ApiResponse" },
813
- {
814
- "type": "object",
815
- "properties": {
816
- "data": { "type": "null" }
817
- },
818
- "required": ["data"]
819
- }
820
- ]
821
- }
822
- }
823
- }
824
- },
825
- "401": {
826
- "description": "Authentication required.",
827
- "content": {
828
- "application/json": {
829
- "schema": {
830
- "allOf": [
831
- { "$ref": "#/components/schemas/ApiResponse" },
832
- {
833
- "type": "object",
834
- "properties": {
835
- "data": { "type": "null" }
836
- },
837
- "required": ["data"]
838
- }
839
- ]
840
- }
841
- }
842
- }
843
- },
844
- "403": {
845
- "description": "Authenticated user not found.",
846
- "content": {
847
- "application/json": {
848
- "schema": {
849
- "allOf": [
850
- { "$ref": "#/components/schemas/ApiResponse" },
851
- {
852
- "type": "object",
853
- "properties": {
854
- "data": { "type": "null" }
855
- },
856
- "required": ["data"]
857
- }
858
- ]
859
- }
860
- }
861
- }
862
- },
863
- "404": {
864
- "description": "Passkey not found.",
865
- "content": {
866
- "application/json": {
867
- "schema": {
868
- "allOf": [
869
- { "$ref": "#/components/schemas/ApiResponse" },
870
- {
871
- "type": "object",
872
- "properties": {
873
- "data": { "type": "null" }
874
- },
875
- "required": ["data"]
876
- }
877
- ]
878
- }
879
- }
880
- }
881
- },
882
- "501": {
883
- "description": "Passkey management not configured.",
884
- "content": {
885
- "application/json": {
886
- "schema": {
887
- "allOf": [
888
- { "$ref": "#/components/schemas/ApiResponse" },
889
- {
890
- "type": "object",
891
- "properties": {
892
- "data": { "type": "null" }
893
- },
894
- "required": ["data"]
895
- }
896
- ]
897
- }
898
- }
899
- }
900
- }
901
- }
902
763
  }
903
764
  },
904
765
  "/api/auth/v1/passkeys/{credentialId}": {
@@ -1431,7 +1292,7 @@
1431
1292
  },
1432
1293
  "ApiResponse": {
1433
1294
  "type": "object",
1434
- "description": "Standard envelope used by all endpoints (success or error). Mirrors ApiClientBase.ApiResponseData.",
1295
+ "description": "ApiResponse<T> standard envelope used by all endpoints (success or error). T is the endpoint-specific payload type carried in the data field. Mirrors ApiClientBase.ApiResponseData.",
1435
1296
  "properties": {
1436
1297
  "success": {
1437
1298
  "type": "boolean",
@@ -1463,6 +1324,10 @@
1463
1324
  "type": "object",
1464
1325
  "description": "Data payload returned by GET /api/v1/ping.",
1465
1326
  "properties": {
1327
+ "success": {
1328
+ "type": "boolean",
1329
+ "description": "Always true (redundant with ApiResponse.success)."
1330
+ },
1466
1331
  "status": {
1467
1332
  "type": "string",
1468
1333
  "description": "Health indicator (\"ok\")."
@@ -1477,15 +1342,16 @@
1477
1342
  "type": "number"
1478
1343
  },
1479
1344
  "startedAt": {
1480
- "type": "string",
1481
- "format": "date-time"
1345
+ "type": "integer",
1346
+ "format": "int64",
1347
+ "description": "Server start time as milliseconds since epoch."
1482
1348
  },
1483
1349
  "timestamp": {
1484
1350
  "type": "string",
1485
1351
  "format": "date-time"
1486
1352
  }
1487
1353
  },
1488
- "required": ["status", "uptimeSec", "startedAt", "timestamp"]
1354
+ "required": ["success", "status", "uptimeSec", "startedAt", "timestamp"]
1489
1355
  },
1490
1356
  "AuthTokensResponseData": {
1491
1357
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/api-server-base",
3
- "version": "2.0.0-beta.17",
3
+ "version": "2.0.0-beta.19",
4
4
  "description": "Api Server Skeleton / Base Class",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",
@@ -70,34 +70,34 @@
70
70
  "@simplewebauthn/server": "^13.2.2",
71
71
  "@types/cookie-parser": "^1.4.10",
72
72
  "@types/cors": "^2.8.19",
73
- "@types/express": "^4.17.25",
73
+ "@types/express": "^5.0.6",
74
74
  "@types/jsonwebtoken": "^9.0.10",
75
- "@types/multer": "^1.4.13",
76
- "bcryptjs": "^2.4.3",
75
+ "@types/multer": "^2.0.0",
76
+ "bcryptjs": "^3.0.3",
77
77
  "cookie-parser": "^1.4.7",
78
- "cors": "^2.8.5",
79
- "express": "^4.22.1",
78
+ "cors": "^2.8.6",
79
+ "express": "^5.2.1",
80
80
  "jsonwebtoken": "^9.0.3",
81
81
  "multer": "^2.0.2"
82
82
  },
83
83
  "devDependencies": {
84
- "@types/bcryptjs": "^2.4.6",
85
- "@types/express-serve-static-core": "^5.1.0",
84
+ "@types/express-serve-static-core": "^5.1.1",
86
85
  "@types/supertest": "^6.0.3",
87
86
  "@typescript-eslint/eslint-plugin": "^8.54.0",
88
87
  "@typescript-eslint/parser": "^8.54.0",
88
+ "@vitest/coverage-v8": "4.0.18",
89
89
  "eslint": "^9.39.2",
90
90
  "eslint-config-prettier": "^10.1.8",
91
91
  "eslint-plugin-import": "^2.32.0",
92
92
  "jsonc-eslint-parser": "^2.4.2",
93
- "mysql2": "^3.16.0",
94
- "pg": "^8.16.3",
93
+ "mysql2": "^3.16.3",
94
+ "pg": "^8.18.0",
95
95
  "prettier": "^3.8.1",
96
96
  "sequelize": "^6.37.7",
97
97
  "sqlite3": "^5.1.7",
98
- "supertest": "^7.1.4",
98
+ "supertest": "^7.2.2",
99
99
  "typescript": "^5.9.3",
100
- "vitest": "^4.0.16"
100
+ "vitest": "^4.0.18"
101
101
  },
102
102
  "peerDependencies": {
103
103
  "mysql2": "^3.16.0",