@technomoron/api-server-base 1.0.42 → 1.0.43
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/README.txt +4 -0
- package/dist/cjs/api-server-base.cjs +134 -1
- package/dist/cjs/api-server-base.d.ts +2 -0
- package/dist/esm/api-server-base.d.ts +2 -0
- package/dist/esm/api-server-base.js +134 -1
- package/package.json +13 -13
package/README.txt
CHANGED
|
@@ -105,6 +105,10 @@ Request Lifecycle
|
|
|
105
105
|
5. The handler executes and returns its tuple. Responses are normalized to { code, message, data } JSON.
|
|
106
106
|
6. Errors bubble into the wrapper. ApiError instances respect the provided status codes; other exceptions result in a 500 with text derived from guessExceptionText.
|
|
107
107
|
|
|
108
|
+
Client IP Helpers
|
|
109
|
+
-----------------
|
|
110
|
+
Use getClientIp(req) to obtain the most likely client address, skipping loopback entries collected from proxy headers. Call getClientIpChain(req) when you need the de-duplicated sequence gathered from the standard Forwarded/X-Forwarded-For/X-Real-IP headers as well as Express' req.ip/req.ips and the underlying socket.
|
|
111
|
+
|
|
108
112
|
Extending the Base Classes
|
|
109
113
|
--------------------------
|
|
110
114
|
Override getApiKey, getUser, authenticateUser, storeToken, getToken, updateToken, deleteToken, and verifyPassword to integrate with your persistence layer.
|
|
@@ -62,6 +62,76 @@ function hydrateGetBody(req) {
|
|
|
62
62
|
}
|
|
63
63
|
req.body = { ...query, ...body };
|
|
64
64
|
}
|
|
65
|
+
function normalizeIpAddress(candidate) {
|
|
66
|
+
let value = candidate.trim();
|
|
67
|
+
if (!value) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
value = value.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, '');
|
|
71
|
+
if (value.startsWith('::ffff:')) {
|
|
72
|
+
value = value.slice(7);
|
|
73
|
+
}
|
|
74
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
75
|
+
value = value.slice(1, -1);
|
|
76
|
+
}
|
|
77
|
+
const firstColon = value.indexOf(':');
|
|
78
|
+
const lastColon = value.lastIndexOf(':');
|
|
79
|
+
if (firstColon !== -1 && firstColon === lastColon) {
|
|
80
|
+
const maybePort = value.slice(lastColon + 1);
|
|
81
|
+
if (/^\d+$/.test(maybePort)) {
|
|
82
|
+
value = value.slice(0, lastColon);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
value = value.trim();
|
|
86
|
+
return value || null;
|
|
87
|
+
}
|
|
88
|
+
function extractForwardedFor(header) {
|
|
89
|
+
if (!header) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const values = Array.isArray(header) ? header : [header];
|
|
93
|
+
const ips = [];
|
|
94
|
+
for (const entry of values) {
|
|
95
|
+
for (const part of entry.split(',')) {
|
|
96
|
+
const normalized = normalizeIpAddress(part);
|
|
97
|
+
if (normalized) {
|
|
98
|
+
ips.push(normalized);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return ips;
|
|
103
|
+
}
|
|
104
|
+
function extractForwardedHeader(header) {
|
|
105
|
+
if (!header) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const values = Array.isArray(header) ? header : [header];
|
|
109
|
+
const ips = [];
|
|
110
|
+
for (const entry of values) {
|
|
111
|
+
for (const part of entry.split(',')) {
|
|
112
|
+
const match = part.match(/for=([^;]+)/i);
|
|
113
|
+
if (match) {
|
|
114
|
+
const normalized = normalizeIpAddress(match[1]);
|
|
115
|
+
if (normalized) {
|
|
116
|
+
ips.push(normalized);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return ips;
|
|
122
|
+
}
|
|
123
|
+
function isLoopbackAddress(ip) {
|
|
124
|
+
if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (ip === '0.0.0.0' || ip === '127.0.0.1') {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
if (ip.startsWith('127.')) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
65
135
|
class ApiError extends Error {
|
|
66
136
|
constructor({ code, message, data, errors }) {
|
|
67
137
|
const msg = guess_exception_text(message, '[Unknown error (null/undefined)]');
|
|
@@ -174,27 +244,36 @@ class ApiServer {
|
|
|
174
244
|
}
|
|
175
245
|
}
|
|
176
246
|
async getApiKey(token) {
|
|
247
|
+
void token;
|
|
177
248
|
return null;
|
|
178
249
|
}
|
|
179
250
|
async getUser(uid) {
|
|
251
|
+
void uid;
|
|
180
252
|
throw new Error('getUser() not implemented');
|
|
181
253
|
}
|
|
182
254
|
async authenticateUser(params) {
|
|
255
|
+
void params;
|
|
183
256
|
throw new Error('authenticateUser() not implemented');
|
|
184
257
|
}
|
|
185
258
|
async storeToken(params) {
|
|
259
|
+
void params;
|
|
186
260
|
throw new Error('storeToken() not implemented');
|
|
187
261
|
}
|
|
188
262
|
async getToken(params) {
|
|
263
|
+
void params;
|
|
189
264
|
throw new Error('getToken() not implemented');
|
|
190
265
|
}
|
|
191
266
|
async updateToken(params) {
|
|
267
|
+
void params;
|
|
192
268
|
throw new Error('updateToken() not implemented');
|
|
193
269
|
}
|
|
194
270
|
async deleteToken(params) {
|
|
271
|
+
void params;
|
|
195
272
|
throw new Error('deleteToken() not implemented');
|
|
196
273
|
}
|
|
197
274
|
async verifyPassword(password, hash) {
|
|
275
|
+
void password;
|
|
276
|
+
void hash;
|
|
198
277
|
throw new Error('verifyPassword() not implemented');
|
|
199
278
|
}
|
|
200
279
|
filterUser(fullUser) {
|
|
@@ -203,7 +282,10 @@ class ApiServer {
|
|
|
203
282
|
guessExceptionText(error, defMsg = 'Unkown Error') {
|
|
204
283
|
return guess_exception_text(error, defMsg);
|
|
205
284
|
}
|
|
206
|
-
async authorize(apiReq, requiredClass) {
|
|
285
|
+
async authorize(apiReq, requiredClass) {
|
|
286
|
+
void apiReq;
|
|
287
|
+
void requiredClass;
|
|
288
|
+
}
|
|
207
289
|
middlewares() {
|
|
208
290
|
this.app.use(express_1.default.json());
|
|
209
291
|
this.app.use((0, cookie_parser_1.default)());
|
|
@@ -320,6 +402,7 @@ class ApiServer {
|
|
|
320
402
|
}
|
|
321
403
|
handle_request(handler, auth) {
|
|
322
404
|
return async (req, res, next) => {
|
|
405
|
+
void next;
|
|
323
406
|
try {
|
|
324
407
|
const apiReq = (this.currReq = {
|
|
325
408
|
server: this,
|
|
@@ -359,6 +442,56 @@ class ApiServer {
|
|
|
359
442
|
}
|
|
360
443
|
};
|
|
361
444
|
}
|
|
445
|
+
getClientIp(req) {
|
|
446
|
+
const chain = this.getClientIpChain(req);
|
|
447
|
+
for (const ip of chain) {
|
|
448
|
+
if (!isLoopbackAddress(ip)) {
|
|
449
|
+
return ip;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return chain[0] ?? null;
|
|
453
|
+
}
|
|
454
|
+
getClientIpChain(req) {
|
|
455
|
+
const seen = new Set();
|
|
456
|
+
const result = [];
|
|
457
|
+
const pushNormalized = (ip) => {
|
|
458
|
+
if (!ip || seen.has(ip)) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
seen.add(ip);
|
|
462
|
+
result.push(ip);
|
|
463
|
+
};
|
|
464
|
+
for (const ip of extractForwardedFor(req.headers['x-forwarded-for'])) {
|
|
465
|
+
pushNormalized(ip);
|
|
466
|
+
}
|
|
467
|
+
for (const ip of extractForwardedHeader(req.headers['forwarded'])) {
|
|
468
|
+
pushNormalized(ip);
|
|
469
|
+
}
|
|
470
|
+
const realIp = req.headers['x-real-ip'];
|
|
471
|
+
if (Array.isArray(realIp)) {
|
|
472
|
+
realIp.forEach((value) => pushNormalized(normalizeIpAddress(value)));
|
|
473
|
+
}
|
|
474
|
+
else if (typeof realIp === 'string') {
|
|
475
|
+
pushNormalized(normalizeIpAddress(realIp));
|
|
476
|
+
}
|
|
477
|
+
if (Array.isArray(req.ips)) {
|
|
478
|
+
for (const ip of req.ips) {
|
|
479
|
+
pushNormalized(normalizeIpAddress(ip));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (typeof req.ip === 'string') {
|
|
483
|
+
pushNormalized(normalizeIpAddress(req.ip));
|
|
484
|
+
}
|
|
485
|
+
const socketAddress = req.socket?.remoteAddress;
|
|
486
|
+
if (typeof socketAddress === 'string') {
|
|
487
|
+
pushNormalized(normalizeIpAddress(socketAddress));
|
|
488
|
+
}
|
|
489
|
+
const connectionAddress = req.connection?.remoteAddress;
|
|
490
|
+
if (typeof connectionAddress === 'string') {
|
|
491
|
+
pushNormalized(normalizeIpAddress(connectionAddress));
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
362
495
|
api(module) {
|
|
363
496
|
const router = express_1.default.Router();
|
|
364
497
|
module.server = this;
|
|
@@ -146,6 +146,8 @@ export declare class ApiServer {
|
|
|
146
146
|
private verifyJWT;
|
|
147
147
|
private authenticate;
|
|
148
148
|
private handle_request;
|
|
149
|
+
getClientIp(req: RequestWithStuff): string | null;
|
|
150
|
+
getClientIpChain(req: RequestWithStuff): string[];
|
|
149
151
|
api<T extends ApiModule<any>>(module: T): this;
|
|
150
152
|
dumpRequest(apiReq: ApiRequest): void;
|
|
151
153
|
}
|
|
@@ -146,6 +146,8 @@ export declare class ApiServer {
|
|
|
146
146
|
private verifyJWT;
|
|
147
147
|
private authenticate;
|
|
148
148
|
private handle_request;
|
|
149
|
+
getClientIp(req: RequestWithStuff): string | null;
|
|
150
|
+
getClientIpChain(req: RequestWithStuff): string[];
|
|
149
151
|
api<T extends ApiModule<any>>(module: T): this;
|
|
150
152
|
dumpRequest(apiReq: ApiRequest): void;
|
|
151
153
|
}
|
|
@@ -55,6 +55,76 @@ function hydrateGetBody(req) {
|
|
|
55
55
|
}
|
|
56
56
|
req.body = { ...query, ...body };
|
|
57
57
|
}
|
|
58
|
+
function normalizeIpAddress(candidate) {
|
|
59
|
+
let value = candidate.trim();
|
|
60
|
+
if (!value) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
value = value.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, '');
|
|
64
|
+
if (value.startsWith('::ffff:')) {
|
|
65
|
+
value = value.slice(7);
|
|
66
|
+
}
|
|
67
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
68
|
+
value = value.slice(1, -1);
|
|
69
|
+
}
|
|
70
|
+
const firstColon = value.indexOf(':');
|
|
71
|
+
const lastColon = value.lastIndexOf(':');
|
|
72
|
+
if (firstColon !== -1 && firstColon === lastColon) {
|
|
73
|
+
const maybePort = value.slice(lastColon + 1);
|
|
74
|
+
if (/^\d+$/.test(maybePort)) {
|
|
75
|
+
value = value.slice(0, lastColon);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
value = value.trim();
|
|
79
|
+
return value || null;
|
|
80
|
+
}
|
|
81
|
+
function extractForwardedFor(header) {
|
|
82
|
+
if (!header) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const values = Array.isArray(header) ? header : [header];
|
|
86
|
+
const ips = [];
|
|
87
|
+
for (const entry of values) {
|
|
88
|
+
for (const part of entry.split(',')) {
|
|
89
|
+
const normalized = normalizeIpAddress(part);
|
|
90
|
+
if (normalized) {
|
|
91
|
+
ips.push(normalized);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return ips;
|
|
96
|
+
}
|
|
97
|
+
function extractForwardedHeader(header) {
|
|
98
|
+
if (!header) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const values = Array.isArray(header) ? header : [header];
|
|
102
|
+
const ips = [];
|
|
103
|
+
for (const entry of values) {
|
|
104
|
+
for (const part of entry.split(',')) {
|
|
105
|
+
const match = part.match(/for=([^;]+)/i);
|
|
106
|
+
if (match) {
|
|
107
|
+
const normalized = normalizeIpAddress(match[1]);
|
|
108
|
+
if (normalized) {
|
|
109
|
+
ips.push(normalized);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return ips;
|
|
115
|
+
}
|
|
116
|
+
function isLoopbackAddress(ip) {
|
|
117
|
+
if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (ip === '0.0.0.0' || ip === '127.0.0.1') {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (ip.startsWith('127.')) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
58
128
|
export class ApiError extends Error {
|
|
59
129
|
constructor({ code, message, data, errors }) {
|
|
60
130
|
const msg = guess_exception_text(message, '[Unknown error (null/undefined)]');
|
|
@@ -166,27 +236,36 @@ export class ApiServer {
|
|
|
166
236
|
}
|
|
167
237
|
}
|
|
168
238
|
async getApiKey(token) {
|
|
239
|
+
void token;
|
|
169
240
|
return null;
|
|
170
241
|
}
|
|
171
242
|
async getUser(uid) {
|
|
243
|
+
void uid;
|
|
172
244
|
throw new Error('getUser() not implemented');
|
|
173
245
|
}
|
|
174
246
|
async authenticateUser(params) {
|
|
247
|
+
void params;
|
|
175
248
|
throw new Error('authenticateUser() not implemented');
|
|
176
249
|
}
|
|
177
250
|
async storeToken(params) {
|
|
251
|
+
void params;
|
|
178
252
|
throw new Error('storeToken() not implemented');
|
|
179
253
|
}
|
|
180
254
|
async getToken(params) {
|
|
255
|
+
void params;
|
|
181
256
|
throw new Error('getToken() not implemented');
|
|
182
257
|
}
|
|
183
258
|
async updateToken(params) {
|
|
259
|
+
void params;
|
|
184
260
|
throw new Error('updateToken() not implemented');
|
|
185
261
|
}
|
|
186
262
|
async deleteToken(params) {
|
|
263
|
+
void params;
|
|
187
264
|
throw new Error('deleteToken() not implemented');
|
|
188
265
|
}
|
|
189
266
|
async verifyPassword(password, hash) {
|
|
267
|
+
void password;
|
|
268
|
+
void hash;
|
|
190
269
|
throw new Error('verifyPassword() not implemented');
|
|
191
270
|
}
|
|
192
271
|
filterUser(fullUser) {
|
|
@@ -195,7 +274,10 @@ export class ApiServer {
|
|
|
195
274
|
guessExceptionText(error, defMsg = 'Unkown Error') {
|
|
196
275
|
return guess_exception_text(error, defMsg);
|
|
197
276
|
}
|
|
198
|
-
async authorize(apiReq, requiredClass) {
|
|
277
|
+
async authorize(apiReq, requiredClass) {
|
|
278
|
+
void apiReq;
|
|
279
|
+
void requiredClass;
|
|
280
|
+
}
|
|
199
281
|
middlewares() {
|
|
200
282
|
this.app.use(express.json());
|
|
201
283
|
this.app.use(cookieParser());
|
|
@@ -312,6 +394,7 @@ export class ApiServer {
|
|
|
312
394
|
}
|
|
313
395
|
handle_request(handler, auth) {
|
|
314
396
|
return async (req, res, next) => {
|
|
397
|
+
void next;
|
|
315
398
|
try {
|
|
316
399
|
const apiReq = (this.currReq = {
|
|
317
400
|
server: this,
|
|
@@ -351,6 +434,56 @@ export class ApiServer {
|
|
|
351
434
|
}
|
|
352
435
|
};
|
|
353
436
|
}
|
|
437
|
+
getClientIp(req) {
|
|
438
|
+
const chain = this.getClientIpChain(req);
|
|
439
|
+
for (const ip of chain) {
|
|
440
|
+
if (!isLoopbackAddress(ip)) {
|
|
441
|
+
return ip;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return chain[0] ?? null;
|
|
445
|
+
}
|
|
446
|
+
getClientIpChain(req) {
|
|
447
|
+
const seen = new Set();
|
|
448
|
+
const result = [];
|
|
449
|
+
const pushNormalized = (ip) => {
|
|
450
|
+
if (!ip || seen.has(ip)) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
seen.add(ip);
|
|
454
|
+
result.push(ip);
|
|
455
|
+
};
|
|
456
|
+
for (const ip of extractForwardedFor(req.headers['x-forwarded-for'])) {
|
|
457
|
+
pushNormalized(ip);
|
|
458
|
+
}
|
|
459
|
+
for (const ip of extractForwardedHeader(req.headers['forwarded'])) {
|
|
460
|
+
pushNormalized(ip);
|
|
461
|
+
}
|
|
462
|
+
const realIp = req.headers['x-real-ip'];
|
|
463
|
+
if (Array.isArray(realIp)) {
|
|
464
|
+
realIp.forEach((value) => pushNormalized(normalizeIpAddress(value)));
|
|
465
|
+
}
|
|
466
|
+
else if (typeof realIp === 'string') {
|
|
467
|
+
pushNormalized(normalizeIpAddress(realIp));
|
|
468
|
+
}
|
|
469
|
+
if (Array.isArray(req.ips)) {
|
|
470
|
+
for (const ip of req.ips) {
|
|
471
|
+
pushNormalized(normalizeIpAddress(ip));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (typeof req.ip === 'string') {
|
|
475
|
+
pushNormalized(normalizeIpAddress(req.ip));
|
|
476
|
+
}
|
|
477
|
+
const socketAddress = req.socket?.remoteAddress;
|
|
478
|
+
if (typeof socketAddress === 'string') {
|
|
479
|
+
pushNormalized(normalizeIpAddress(socketAddress));
|
|
480
|
+
}
|
|
481
|
+
const connectionAddress = req.connection?.remoteAddress;
|
|
482
|
+
if (typeof connectionAddress === 'string') {
|
|
483
|
+
pushNormalized(normalizeIpAddress(connectionAddress));
|
|
484
|
+
}
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
354
487
|
api(module) {
|
|
355
488
|
const router = express.Router();
|
|
356
489
|
module.server = this;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technomoron/api-server-base",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.43",
|
|
4
4
|
"description": "Api Server Skeleton / Base Class",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.cjs",
|
|
@@ -35,34 +35,34 @@
|
|
|
35
35
|
"lint": "eslint --ext .js,.ts,.vue ./",
|
|
36
36
|
"lintfix": "eslint --fix --ext .js,.ts,.vue ./",
|
|
37
37
|
"format": "eslint --fix --ext .js,.ts,.vue ./ && prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\"",
|
|
38
|
-
"cleanbuild": "rm -rf ./dist/ && eslint --fix --ext .js,.ts,.vue ./ && prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\" &&
|
|
38
|
+
"cleanbuild": "rm -rf ./dist/ && eslint --fix --ext .js,.ts,.vue ./ && prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\" && npm run build",
|
|
39
39
|
"pretty": "prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\""
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@types/cookie-parser": "^1.4.9",
|
|
43
43
|
"@types/cors": "^2.8.19",
|
|
44
|
-
"@types/express": "^4.17.
|
|
45
|
-
"@types/jsonwebtoken": "^9.0.
|
|
44
|
+
"@types/express": "^4.17.23",
|
|
45
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
46
46
|
"@types/multer": "^1.4.13",
|
|
47
47
|
"cookie-parser": "^1.4.7",
|
|
48
48
|
"cors": "^2.8.5",
|
|
49
49
|
"express": "^4.21.2",
|
|
50
50
|
"jsonwebtoken": "^9.0.2",
|
|
51
|
-
"multer": "^2.0.
|
|
51
|
+
"multer": "^2.0.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@types/express-serve-static-core": "^5.0
|
|
54
|
+
"@types/express-serve-static-core": "^5.1.0",
|
|
55
55
|
"@types/supertest": "^6.0.3",
|
|
56
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
57
|
-
"@typescript-eslint/parser": "^8.
|
|
56
|
+
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
|
57
|
+
"@typescript-eslint/parser": "^8.46.1",
|
|
58
58
|
"@vue/eslint-config-prettier": "10.2.0",
|
|
59
59
|
"@vue/eslint-config-typescript": "14.5.0",
|
|
60
|
-
"eslint": "^9.
|
|
61
|
-
"eslint-plugin-import": "^2.
|
|
62
|
-
"eslint-plugin-vue": "^10.1
|
|
63
|
-
"prettier": "^3.
|
|
60
|
+
"eslint": "^9.37.0",
|
|
61
|
+
"eslint-plugin-import": "^2.32.0",
|
|
62
|
+
"eslint-plugin-vue": "^10.5.1",
|
|
63
|
+
"prettier": "^3.6.2",
|
|
64
64
|
"supertest": "^7.1.4",
|
|
65
|
-
"typescript": "^5.
|
|
65
|
+
"typescript": "^5.9.3",
|
|
66
66
|
"vitest": "^3.2.4"
|
|
67
67
|
},
|
|
68
68
|
"files": [
|