@technomoron/api-server-base 1.0.42 → 1.1.0
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 +12 -3
- package/dist/cjs/api-server-base.cjs +194 -13
- package/dist/cjs/api-server-base.d.ts +31 -24
- package/dist/cjs/auth-module.cjs +26 -0
- package/dist/cjs/auth-module.d.ts +17 -0
- package/dist/cjs/auth-storage.cjs +87 -0
- package/dist/cjs/auth-storage.d.ts +114 -0
- package/dist/cjs/index.cjs +7 -1
- package/dist/cjs/index.d.ts +4 -0
- package/dist/esm/api-server-base.d.ts +31 -24
- package/dist/esm/api-server-base.js +194 -13
- package/dist/esm/auth-module.d.ts +17 -0
- package/dist/esm/auth-module.js +22 -0
- package/dist/esm/auth-storage.d.ts +114 -0
- package/dist/esm/auth-storage.js +83 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +2 -0
- package/package.json +13 -13
package/README.txt
CHANGED
|
@@ -62,13 +62,18 @@ class UserModule extends ApiModule<AppServer> {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
const yourStorageAdapter = new YourStorageAdapter();
|
|
66
|
+
|
|
65
67
|
const server = new AppServer({
|
|
66
68
|
apiPort: 3101,
|
|
67
69
|
apiHost: '127.0.0.1',
|
|
68
|
-
accessSecret: 'replace-me'
|
|
69
|
-
})
|
|
70
|
+
accessSecret: 'replace-me'
|
|
71
|
+
})
|
|
72
|
+
.authStorage(yourStorageAdapter)
|
|
73
|
+
.api(new UserModule())
|
|
74
|
+
.start();
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
Need a dedicated auth module as well? Chain `.authModule(...)` in the same spot.
|
|
72
77
|
|
|
73
78
|
Handlers must return a tuple: [statusCode], [statusCode, data], or [statusCode, data, message]. Throw ApiError for predictable failures.
|
|
74
79
|
|
|
@@ -105,6 +110,10 @@ Request Lifecycle
|
|
|
105
110
|
5. The handler executes and returns its tuple. Responses are normalized to { code, message, data } JSON.
|
|
106
111
|
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
112
|
|
|
113
|
+
Client IP Helpers
|
|
114
|
+
-----------------
|
|
115
|
+
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.
|
|
116
|
+
|
|
108
117
|
Extending the Base Classes
|
|
109
118
|
--------------------------
|
|
110
119
|
Override getApiKey, getUser, authenticateUser, storeToken, getToken, updateToken, deleteToken, and verifyPassword to integrate with your persistence layer.
|
|
@@ -15,6 +15,8 @@ const cors_1 = __importDefault(require("cors"));
|
|
|
15
15
|
const express_1 = __importDefault(require("express"));
|
|
16
16
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
17
17
|
const multer_1 = __importDefault(require("multer"));
|
|
18
|
+
const auth_module_js_1 = require("./auth-module.cjs");
|
|
19
|
+
const auth_storage_js_1 = require("./auth-storage.cjs");
|
|
18
20
|
class ApiModule {
|
|
19
21
|
constructor(opts = {}) {
|
|
20
22
|
this.mountpath = '';
|
|
@@ -62,6 +64,76 @@ function hydrateGetBody(req) {
|
|
|
62
64
|
}
|
|
63
65
|
req.body = { ...query, ...body };
|
|
64
66
|
}
|
|
67
|
+
function normalizeIpAddress(candidate) {
|
|
68
|
+
let value = candidate.trim();
|
|
69
|
+
if (!value) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
value = value.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, '');
|
|
73
|
+
if (value.startsWith('::ffff:')) {
|
|
74
|
+
value = value.slice(7);
|
|
75
|
+
}
|
|
76
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
77
|
+
value = value.slice(1, -1);
|
|
78
|
+
}
|
|
79
|
+
const firstColon = value.indexOf(':');
|
|
80
|
+
const lastColon = value.lastIndexOf(':');
|
|
81
|
+
if (firstColon !== -1 && firstColon === lastColon) {
|
|
82
|
+
const maybePort = value.slice(lastColon + 1);
|
|
83
|
+
if (/^\d+$/.test(maybePort)) {
|
|
84
|
+
value = value.slice(0, lastColon);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
value = value.trim();
|
|
88
|
+
return value || null;
|
|
89
|
+
}
|
|
90
|
+
function extractForwardedFor(header) {
|
|
91
|
+
if (!header) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const values = Array.isArray(header) ? header : [header];
|
|
95
|
+
const ips = [];
|
|
96
|
+
for (const entry of values) {
|
|
97
|
+
for (const part of entry.split(',')) {
|
|
98
|
+
const normalized = normalizeIpAddress(part);
|
|
99
|
+
if (normalized) {
|
|
100
|
+
ips.push(normalized);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return ips;
|
|
105
|
+
}
|
|
106
|
+
function extractForwardedHeader(header) {
|
|
107
|
+
if (!header) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const values = Array.isArray(header) ? header : [header];
|
|
111
|
+
const ips = [];
|
|
112
|
+
for (const entry of values) {
|
|
113
|
+
for (const part of entry.split(',')) {
|
|
114
|
+
const match = part.match(/for=([^;]+)/i);
|
|
115
|
+
if (match) {
|
|
116
|
+
const normalized = normalizeIpAddress(match[1]);
|
|
117
|
+
if (normalized) {
|
|
118
|
+
ips.push(normalized);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return ips;
|
|
124
|
+
}
|
|
125
|
+
function isLoopbackAddress(ip) {
|
|
126
|
+
if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (ip === '0.0.0.0' || ip === '127.0.0.1') {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if (ip.startsWith('127.')) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
65
137
|
class ApiError extends Error {
|
|
66
138
|
constructor({ code, message, data, errors }) {
|
|
67
139
|
const msg = guess_exception_text(message, '[Unknown error (null/undefined)]');
|
|
@@ -98,6 +170,8 @@ class ApiServer {
|
|
|
98
170
|
constructor(config = {}) {
|
|
99
171
|
this.currReq = null;
|
|
100
172
|
this.config = fillConfig(config);
|
|
173
|
+
this.storageAdapter = auth_storage_js_1.nullAuthStorage;
|
|
174
|
+
this.moduleAdapter = auth_module_js_1.nullAuthModule;
|
|
101
175
|
this.app = (0, express_1.default)();
|
|
102
176
|
if (config.uploadPath) {
|
|
103
177
|
const upload = (0, multer_1.default)({ dest: config.uploadPath });
|
|
@@ -106,6 +180,32 @@ class ApiServer {
|
|
|
106
180
|
this.middlewares();
|
|
107
181
|
// addSwaggerUi(this.app);
|
|
108
182
|
}
|
|
183
|
+
authStorage(storage) {
|
|
184
|
+
this.storageAdapter = storage;
|
|
185
|
+
return this;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
189
|
+
*/
|
|
190
|
+
useAuthStorage(storage) {
|
|
191
|
+
return this.authStorage(storage);
|
|
192
|
+
}
|
|
193
|
+
authModule(module) {
|
|
194
|
+
this.moduleAdapter = module;
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
199
|
+
*/
|
|
200
|
+
useAuthModule(module) {
|
|
201
|
+
return this.authModule(module);
|
|
202
|
+
}
|
|
203
|
+
getAuthStorage() {
|
|
204
|
+
return this.storageAdapter;
|
|
205
|
+
}
|
|
206
|
+
getAuthModule() {
|
|
207
|
+
return this.moduleAdapter;
|
|
208
|
+
}
|
|
109
209
|
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
110
210
|
options || (options = {});
|
|
111
211
|
const opts = { ...options, expiresIn: expiresInSeconds };
|
|
@@ -174,36 +274,63 @@ class ApiServer {
|
|
|
174
274
|
}
|
|
175
275
|
}
|
|
176
276
|
async getApiKey(token) {
|
|
277
|
+
void token;
|
|
177
278
|
return null;
|
|
178
279
|
}
|
|
179
280
|
async getUser(uid) {
|
|
180
|
-
|
|
281
|
+
return this.storageAdapter.getUser(uid);
|
|
282
|
+
}
|
|
283
|
+
getUserPasswordHash(user) {
|
|
284
|
+
return this.storageAdapter.getUserPasswordHash(user);
|
|
285
|
+
}
|
|
286
|
+
getUserId(user) {
|
|
287
|
+
return this.storageAdapter.getUserId(user);
|
|
181
288
|
}
|
|
182
289
|
async authenticateUser(params) {
|
|
183
|
-
|
|
290
|
+
if (!params?.login || !params?.password) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const user = await this.getUser(params.login);
|
|
294
|
+
if (!user) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const hash = this.storageAdapter.getUserPasswordHash(user);
|
|
298
|
+
return this.verifyPassword(params.password, hash);
|
|
184
299
|
}
|
|
185
|
-
async storeToken(
|
|
186
|
-
|
|
300
|
+
async storeToken(data) {
|
|
301
|
+
await this.storageAdapter.storeToken(data);
|
|
187
302
|
}
|
|
188
|
-
async getToken(
|
|
189
|
-
|
|
303
|
+
async getToken(query) {
|
|
304
|
+
return this.storageAdapter.getToken(query);
|
|
190
305
|
}
|
|
191
|
-
async updateToken(
|
|
192
|
-
|
|
306
|
+
async updateToken(updates) {
|
|
307
|
+
if (typeof this.storageAdapter.updateToken !== 'function') {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return this.storageAdapter.updateToken({
|
|
311
|
+
refreshToken: updates.refreshToken,
|
|
312
|
+
access: updates.accessToken,
|
|
313
|
+
expires: updates.expires,
|
|
314
|
+
clientId: updates.clientId,
|
|
315
|
+
scope: updates.scope
|
|
316
|
+
});
|
|
193
317
|
}
|
|
194
|
-
async deleteToken(
|
|
195
|
-
|
|
318
|
+
async deleteToken(query) {
|
|
319
|
+
return this.storageAdapter.deleteToken(query);
|
|
196
320
|
}
|
|
197
321
|
async verifyPassword(password, hash) {
|
|
198
|
-
|
|
322
|
+
return this.storageAdapter.verifyPassword(password, hash);
|
|
199
323
|
}
|
|
200
324
|
filterUser(fullUser) {
|
|
201
|
-
return fullUser;
|
|
325
|
+
return this.storageAdapter.filterUser(fullUser);
|
|
202
326
|
}
|
|
203
327
|
guessExceptionText(error, defMsg = 'Unkown Error') {
|
|
204
328
|
return guess_exception_text(error, defMsg);
|
|
205
329
|
}
|
|
206
|
-
async authorize(apiReq, requiredClass) {
|
|
330
|
+
async authorize(apiReq, requiredClass) {
|
|
331
|
+
void apiReq;
|
|
332
|
+
void requiredClass;
|
|
333
|
+
}
|
|
207
334
|
middlewares() {
|
|
208
335
|
this.app.use(express_1.default.json());
|
|
209
336
|
this.app.use((0, cookie_parser_1.default)());
|
|
@@ -320,6 +447,7 @@ class ApiServer {
|
|
|
320
447
|
}
|
|
321
448
|
handle_request(handler, auth) {
|
|
322
449
|
return async (req, res, next) => {
|
|
450
|
+
void next;
|
|
323
451
|
try {
|
|
324
452
|
const apiReq = (this.currReq = {
|
|
325
453
|
server: this,
|
|
@@ -359,9 +487,62 @@ class ApiServer {
|
|
|
359
487
|
}
|
|
360
488
|
};
|
|
361
489
|
}
|
|
490
|
+
getClientIp(req) {
|
|
491
|
+
const chain = this.getClientIpChain(req);
|
|
492
|
+
for (const ip of chain) {
|
|
493
|
+
if (!isLoopbackAddress(ip)) {
|
|
494
|
+
return ip;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return chain[0] ?? null;
|
|
498
|
+
}
|
|
499
|
+
getClientIpChain(req) {
|
|
500
|
+
const seen = new Set();
|
|
501
|
+
const result = [];
|
|
502
|
+
const pushNormalized = (ip) => {
|
|
503
|
+
if (!ip || seen.has(ip)) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
seen.add(ip);
|
|
507
|
+
result.push(ip);
|
|
508
|
+
};
|
|
509
|
+
for (const ip of extractForwardedFor(req.headers['x-forwarded-for'])) {
|
|
510
|
+
pushNormalized(ip);
|
|
511
|
+
}
|
|
512
|
+
for (const ip of extractForwardedHeader(req.headers['forwarded'])) {
|
|
513
|
+
pushNormalized(ip);
|
|
514
|
+
}
|
|
515
|
+
const realIp = req.headers['x-real-ip'];
|
|
516
|
+
if (Array.isArray(realIp)) {
|
|
517
|
+
realIp.forEach((value) => pushNormalized(normalizeIpAddress(value)));
|
|
518
|
+
}
|
|
519
|
+
else if (typeof realIp === 'string') {
|
|
520
|
+
pushNormalized(normalizeIpAddress(realIp));
|
|
521
|
+
}
|
|
522
|
+
if (Array.isArray(req.ips)) {
|
|
523
|
+
for (const ip of req.ips) {
|
|
524
|
+
pushNormalized(normalizeIpAddress(ip));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (typeof req.ip === 'string') {
|
|
528
|
+
pushNormalized(normalizeIpAddress(req.ip));
|
|
529
|
+
}
|
|
530
|
+
const socketAddress = req.socket?.remoteAddress;
|
|
531
|
+
if (typeof socketAddress === 'string') {
|
|
532
|
+
pushNormalized(normalizeIpAddress(socketAddress));
|
|
533
|
+
}
|
|
534
|
+
const connectionAddress = req.connection?.remoteAddress;
|
|
535
|
+
if (typeof connectionAddress === 'string') {
|
|
536
|
+
pushNormalized(normalizeIpAddress(connectionAddress));
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
362
540
|
api(module) {
|
|
363
541
|
const router = express_1.default.Router();
|
|
364
542
|
module.server = this;
|
|
543
|
+
if (module?.moduleType === 'auth') {
|
|
544
|
+
this.authModule(module);
|
|
545
|
+
}
|
|
365
546
|
module.checkConfig();
|
|
366
547
|
const base = this.config.apiBasePath ?? '/api';
|
|
367
548
|
const ns = module.namespace;
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Application, Request, Response } from 'express';
|
|
8
8
|
import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
9
|
+
import type { AuthProviderModule } from './auth-module.js';
|
|
10
|
+
import type { AuthIdentifier, AuthStorage, AuthTokenData, AuthTokenQuery } from './auth-storage.js';
|
|
9
11
|
export type { Application, Request, Response, NextFunction, Router } from 'express';
|
|
10
12
|
export type { Multer } from 'multer';
|
|
11
13
|
export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
@@ -104,39 +106,42 @@ export declare class ApiServer {
|
|
|
104
106
|
app: Application;
|
|
105
107
|
currReq: ApiRequest | null;
|
|
106
108
|
readonly config: ApiServerConf;
|
|
109
|
+
private storageAdapter;
|
|
110
|
+
private moduleAdapter;
|
|
107
111
|
constructor(config?: Partial<ApiServerConf>);
|
|
112
|
+
authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
113
|
+
/**
|
|
114
|
+
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
115
|
+
*/
|
|
116
|
+
useAuthStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
117
|
+
authModule<UserRow, SafeUser>(module: AuthProviderModule<UserRow, SafeUser>): this;
|
|
118
|
+
/**
|
|
119
|
+
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
120
|
+
*/
|
|
121
|
+
useAuthModule<UserRow, SafeUser>(module: AuthProviderModule<UserRow, SafeUser>): this;
|
|
122
|
+
getAuthStorage(): AuthStorage<any, any>;
|
|
123
|
+
getAuthModule(): AuthProviderModule<any, any>;
|
|
108
124
|
jwtSign(payload: any, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
|
|
109
125
|
jwtVerify<T>(token: string, secret: string, options?: VerifyOptions): JwtVerifyResult<T>;
|
|
110
126
|
jwtDecode<T>(token: string, options?: jwt.DecodeOptions): JwtDecodeResult<T>;
|
|
111
127
|
getApiKey<T = ApiKey>(token: string): Promise<T | null>;
|
|
112
|
-
getUser(uid:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
getToken(params: {
|
|
123
|
-
accessToken?: string;
|
|
124
|
-
refreshToken?: string;
|
|
125
|
-
userId?: unknown;
|
|
126
|
-
}): Promise<unknown>;
|
|
127
|
-
updateToken(params: {
|
|
128
|
+
getUser(uid: AuthIdentifier): Promise<unknown>;
|
|
129
|
+
getUserPasswordHash(user: unknown): string;
|
|
130
|
+
getUserId(user: unknown): AuthIdentifier;
|
|
131
|
+
authenticateUser(params: {
|
|
132
|
+
login: string;
|
|
133
|
+
password: string;
|
|
134
|
+
}): Promise<boolean>;
|
|
135
|
+
storeToken(data: AuthTokenData): Promise<void>;
|
|
136
|
+
getToken(query: AuthTokenQuery): Promise<AuthTokenData | null>;
|
|
137
|
+
updateToken(updates: {
|
|
128
138
|
accessToken: string;
|
|
129
139
|
refreshToken: string;
|
|
130
140
|
expires?: Date;
|
|
141
|
+
clientId?: string;
|
|
142
|
+
scope?: string[];
|
|
131
143
|
}): Promise<boolean>;
|
|
132
|
-
deleteToken(
|
|
133
|
-
refreshToken?: string;
|
|
134
|
-
accessToken?: string;
|
|
135
|
-
userId?: unknown;
|
|
136
|
-
domain?: string;
|
|
137
|
-
fingerprint?: string;
|
|
138
|
-
label?: string;
|
|
139
|
-
}): Promise<number>;
|
|
144
|
+
deleteToken(query: AuthTokenQuery): Promise<number>;
|
|
140
145
|
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
141
146
|
filterUser<T = any, U = any>(fullUser: T): U;
|
|
142
147
|
guessExceptionText(error: any, defMsg?: string): string;
|
|
@@ -146,6 +151,8 @@ export declare class ApiServer {
|
|
|
146
151
|
private verifyJWT;
|
|
147
152
|
private authenticate;
|
|
148
153
|
private handle_request;
|
|
154
|
+
getClientIp(req: RequestWithStuff): string | null;
|
|
155
|
+
getClientIpChain(req: RequestWithStuff): string[];
|
|
149
156
|
api<T extends ApiModule<any>>(module: T): this;
|
|
150
157
|
dumpRequest(apiReq: ApiRequest): void;
|
|
151
158
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nullAuthModule = exports.BaseAuthModule = void 0;
|
|
4
|
+
// Handy base that you can extend when wiring a real auth module. Subclasses
|
|
5
|
+
// must provide their namespace. Methods throw by default so unimplemented
|
|
6
|
+
// hooks fail loudly.
|
|
7
|
+
class BaseAuthModule {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.moduleType = 'auth';
|
|
10
|
+
}
|
|
11
|
+
// Override to mint tokens for the provided user and request context
|
|
12
|
+
async issueTokens(apiReq, user, metadata) {
|
|
13
|
+
void apiReq;
|
|
14
|
+
void user;
|
|
15
|
+
void metadata;
|
|
16
|
+
throw new Error('Auth module not configured');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.BaseAuthModule = BaseAuthModule;
|
|
20
|
+
class NullAuthModule extends BaseAuthModule {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(...arguments);
|
|
23
|
+
this.namespace = '__null__';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.nullAuthModule = new NullAuthModule();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApiRequest } from './api-server-base.js';
|
|
2
|
+
import type { AuthTokenMetadata, AuthTokenPair } from './auth-storage.js';
|
|
3
|
+
export interface AuthProviderModule<UserRow, SafeUser> {
|
|
4
|
+
readonly moduleType: 'auth';
|
|
5
|
+
readonly namespace: string;
|
|
6
|
+
issueTokens(apiReq: ApiRequest, user: UserRow, metadata?: AuthTokenMetadata & {
|
|
7
|
+
expires?: Date;
|
|
8
|
+
}): Promise<AuthTokenPair>;
|
|
9
|
+
}
|
|
10
|
+
export declare abstract class BaseAuthModule implements AuthProviderModule<unknown, unknown> {
|
|
11
|
+
readonly moduleType: "auth";
|
|
12
|
+
abstract readonly namespace: string;
|
|
13
|
+
issueTokens(apiReq: ApiRequest, user: unknown, metadata?: AuthTokenMetadata & {
|
|
14
|
+
expires?: Date;
|
|
15
|
+
}): Promise<AuthTokenPair>;
|
|
16
|
+
}
|
|
17
|
+
export declare const nullAuthModule: AuthProviderModule<unknown, unknown>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Numeric database id or lookup string such as username/email.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.nullAuthStorage = exports.BaseAuthStorage = void 0;
|
|
5
|
+
// Handy base you can extend when wiring a real storage adapter. Every method
|
|
6
|
+
// throws by default so unimplemented hooks fail loudly.
|
|
7
|
+
class BaseAuthStorage {
|
|
8
|
+
// Override to load a user record by identifier
|
|
9
|
+
async getUser(identifier) {
|
|
10
|
+
void identifier;
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
// Override to return the stored password hash for the user
|
|
14
|
+
getUserPasswordHash(user) {
|
|
15
|
+
void user;
|
|
16
|
+
throw new Error('Auth storage not configured');
|
|
17
|
+
}
|
|
18
|
+
// Override to expose the canonical user identifier
|
|
19
|
+
getUserId(user) {
|
|
20
|
+
void user;
|
|
21
|
+
throw new Error('Auth storage not configured');
|
|
22
|
+
}
|
|
23
|
+
// Override to strip sensitive fields from the user record
|
|
24
|
+
filterUser(user) {
|
|
25
|
+
return user;
|
|
26
|
+
}
|
|
27
|
+
// Override to validate a raw password against the stored hash
|
|
28
|
+
async verifyPassword(password, hash) {
|
|
29
|
+
void password;
|
|
30
|
+
void hash;
|
|
31
|
+
throw new Error('Auth storage not configured');
|
|
32
|
+
}
|
|
33
|
+
// Override to persist newly issued tokens
|
|
34
|
+
async storeToken(data) {
|
|
35
|
+
void data;
|
|
36
|
+
throw new Error('Auth storage not configured');
|
|
37
|
+
}
|
|
38
|
+
// Override to look up a stored token by query
|
|
39
|
+
async getToken(query) {
|
|
40
|
+
void query;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Override to remove stored tokens that match the query
|
|
44
|
+
async deleteToken(query) {
|
|
45
|
+
void query;
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
// Override to update metadata for an existing refresh token
|
|
49
|
+
async updateToken(updates) {
|
|
50
|
+
void updates;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
// Override to create a new passkey challenge record
|
|
54
|
+
async createPasskeyChallenge(params) {
|
|
55
|
+
void params;
|
|
56
|
+
throw new Error('Auth storage not configured');
|
|
57
|
+
}
|
|
58
|
+
// Override to verify an incoming WebAuthn response
|
|
59
|
+
async verifyPasskeyResponse(params) {
|
|
60
|
+
void params;
|
|
61
|
+
throw new Error('Auth storage not configured');
|
|
62
|
+
}
|
|
63
|
+
// Override to fetch an OAuth client by identifier
|
|
64
|
+
async getClient(clientId) {
|
|
65
|
+
void clientId;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
// Override to compare a provided client secret against storage
|
|
69
|
+
async verifyClientSecret(client, clientSecret) {
|
|
70
|
+
void client;
|
|
71
|
+
void clientSecret;
|
|
72
|
+
throw new Error('Auth storage not configured');
|
|
73
|
+
}
|
|
74
|
+
// Override to create a new authorization code entry
|
|
75
|
+
async createAuthCode(request) {
|
|
76
|
+
void request;
|
|
77
|
+
throw new Error('Auth storage not configured');
|
|
78
|
+
}
|
|
79
|
+
// Override to consume and invalidate an authorization code
|
|
80
|
+
async consumeAuthCode(code, clientId) {
|
|
81
|
+
void code;
|
|
82
|
+
void clientId;
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.BaseAuthStorage = BaseAuthStorage;
|
|
87
|
+
exports.nullAuthStorage = new BaseAuthStorage();
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export type AuthIdentifier = string | number;
|
|
2
|
+
export interface AuthTokenMetadata {
|
|
3
|
+
clientId?: string;
|
|
4
|
+
domain?: string;
|
|
5
|
+
fingerprint?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
scope?: string | string[];
|
|
8
|
+
}
|
|
9
|
+
export interface AuthTokenData extends AuthTokenMetadata {
|
|
10
|
+
access: string;
|
|
11
|
+
expires?: Date;
|
|
12
|
+
refresh: string;
|
|
13
|
+
userId: AuthIdentifier;
|
|
14
|
+
}
|
|
15
|
+
export interface AuthTokenQuery extends AuthTokenMetadata {
|
|
16
|
+
accessToken?: string;
|
|
17
|
+
refreshToken?: string;
|
|
18
|
+
userId?: AuthIdentifier;
|
|
19
|
+
}
|
|
20
|
+
export interface AuthTokenPair {
|
|
21
|
+
accessToken: string;
|
|
22
|
+
refreshToken: string;
|
|
23
|
+
}
|
|
24
|
+
export interface AuthTokenPayload extends AuthTokenMetadata {
|
|
25
|
+
exp?: number;
|
|
26
|
+
iat?: number;
|
|
27
|
+
uid: AuthIdentifier;
|
|
28
|
+
}
|
|
29
|
+
export interface PasskeyChallengeParams extends AuthTokenMetadata {
|
|
30
|
+
action: 'register' | 'authenticate';
|
|
31
|
+
login?: string;
|
|
32
|
+
userAgent?: string;
|
|
33
|
+
userId?: AuthIdentifier;
|
|
34
|
+
}
|
|
35
|
+
export interface PasskeyChallenge extends Record<string, unknown> {
|
|
36
|
+
challenge: string;
|
|
37
|
+
expiresAt?: string | number | Date;
|
|
38
|
+
userId?: AuthIdentifier;
|
|
39
|
+
}
|
|
40
|
+
export interface PasskeyVerificationParams extends AuthTokenMetadata {
|
|
41
|
+
expectedChallenge: string;
|
|
42
|
+
login?: string;
|
|
43
|
+
response: Record<string, unknown>;
|
|
44
|
+
userId?: AuthIdentifier;
|
|
45
|
+
}
|
|
46
|
+
export interface PasskeyVerificationResult extends Record<string, unknown> {
|
|
47
|
+
login?: string;
|
|
48
|
+
tokens?: AuthTokenPair;
|
|
49
|
+
userId?: AuthIdentifier;
|
|
50
|
+
verified: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface OAuthClient {
|
|
53
|
+
clientId: string;
|
|
54
|
+
clientSecret: string;
|
|
55
|
+
firstParty?: boolean;
|
|
56
|
+
metadata?: Record<string, unknown>;
|
|
57
|
+
name?: string;
|
|
58
|
+
redirectUris: string[];
|
|
59
|
+
scope?: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface AuthCodeData {
|
|
62
|
+
code: string;
|
|
63
|
+
clientId: string;
|
|
64
|
+
codeChallenge?: string;
|
|
65
|
+
codeChallengeMethod?: 'plain' | 'S256';
|
|
66
|
+
expiresAt: Date;
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
redirectUri: string;
|
|
69
|
+
scope: string[];
|
|
70
|
+
userId: AuthIdentifier;
|
|
71
|
+
}
|
|
72
|
+
export type AuthCodeRequest = Omit<AuthCodeData, 'code' | 'expiresAt'> & {
|
|
73
|
+
code?: string;
|
|
74
|
+
expiresInSeconds?: number;
|
|
75
|
+
};
|
|
76
|
+
export interface AuthStorage<UserRow, SafeUser> {
|
|
77
|
+
getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
|
|
78
|
+
getUserPasswordHash(user: UserRow): string;
|
|
79
|
+
getUserId(user: UserRow): AuthIdentifier;
|
|
80
|
+
filterUser(user: UserRow): SafeUser;
|
|
81
|
+
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
82
|
+
storeToken(data: AuthTokenData): Promise<void>;
|
|
83
|
+
getToken(query: AuthTokenQuery): Promise<AuthTokenData | null>;
|
|
84
|
+
deleteToken(query: AuthTokenQuery): Promise<number>;
|
|
85
|
+
updateToken?(updates: Partial<AuthTokenData> & {
|
|
86
|
+
refreshToken: string;
|
|
87
|
+
}): Promise<boolean>;
|
|
88
|
+
createPasskeyChallenge?(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
89
|
+
verifyPasskeyResponse?(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
90
|
+
getClient?(clientId: string): Promise<OAuthClient | null>;
|
|
91
|
+
verifyClientSecret?(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
92
|
+
createAuthCode?(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
93
|
+
consumeAuthCode?(code: string, clientId: string): Promise<AuthCodeData | null>;
|
|
94
|
+
}
|
|
95
|
+
export declare class BaseAuthStorage<UserRow = unknown, SafeUser = unknown> implements AuthStorage<UserRow, SafeUser> {
|
|
96
|
+
getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
|
|
97
|
+
getUserPasswordHash(user: UserRow): string;
|
|
98
|
+
getUserId(user: UserRow): AuthIdentifier;
|
|
99
|
+
filterUser(user: UserRow): SafeUser;
|
|
100
|
+
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
101
|
+
storeToken(data: AuthTokenData): Promise<void>;
|
|
102
|
+
getToken(query: AuthTokenQuery): Promise<AuthTokenData | null>;
|
|
103
|
+
deleteToken(query: AuthTokenQuery): Promise<number>;
|
|
104
|
+
updateToken(updates: Partial<AuthTokenData> & {
|
|
105
|
+
refreshToken: string;
|
|
106
|
+
}): Promise<boolean>;
|
|
107
|
+
createPasskeyChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
108
|
+
verifyPasskeyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
109
|
+
getClient(clientId: string): Promise<OAuthClient | null>;
|
|
110
|
+
verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
111
|
+
createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
112
|
+
consumeAuthCode(code: string, clientId: string): Promise<AuthCodeData | null>;
|
|
113
|
+
}
|
|
114
|
+
export declare const nullAuthStorage: AuthStorage<unknown, unknown>;
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -3,9 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ApiError = exports.ApiModule = exports.ApiServer = void 0;
|
|
6
|
+
exports.BaseAuthModule = exports.nullAuthModule = exports.BaseAuthStorage = exports.nullAuthStorage = exports.ApiError = exports.ApiModule = exports.ApiServer = void 0;
|
|
7
7
|
var api_server_base_js_1 = require("./api-server-base.cjs");
|
|
8
8
|
Object.defineProperty(exports, "ApiServer", { enumerable: true, get: function () { return __importDefault(api_server_base_js_1).default; } });
|
|
9
9
|
var api_server_base_js_2 = require("./api-server-base.cjs");
|
|
10
10
|
Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_server_base_js_2.ApiModule; } });
|
|
11
11
|
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return api_server_base_js_2.ApiError; } });
|
|
12
|
+
var auth_storage_js_1 = require("./auth-storage.cjs");
|
|
13
|
+
Object.defineProperty(exports, "nullAuthStorage", { enumerable: true, get: function () { return auth_storage_js_1.nullAuthStorage; } });
|
|
14
|
+
Object.defineProperty(exports, "BaseAuthStorage", { enumerable: true, get: function () { return auth_storage_js_1.BaseAuthStorage; } });
|
|
15
|
+
var auth_module_js_1 = require("./auth-module.cjs");
|
|
16
|
+
Object.defineProperty(exports, "nullAuthModule", { enumerable: true, get: function () { return auth_module_js_1.nullAuthModule; } });
|
|
17
|
+
Object.defineProperty(exports, "BaseAuthModule", { enumerable: true, get: function () { return auth_module_js_1.BaseAuthModule; } });
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { default as ApiServer } from './api-server-base.js';
|
|
2
2
|
export { ApiModule, ApiError } from './api-server-base.js';
|
|
3
3
|
export type { ApiErrorParams, ApiHandler, ApiKey, ApiServerConf, ApiRequest, ApiRoute, ApiAuthType, ApiAuthClass, ApiTokenData, RequestWithStuff } from './api-server-base.js';
|
|
4
|
+
export type { AuthIdentifier, AuthTokenMetadata, AuthTokenData, AuthTokenQuery, AuthTokenPair, AuthTokenPayload, PasskeyChallengeParams, PasskeyChallenge, PasskeyVerificationParams, PasskeyVerificationResult, OAuthClient, AuthCodeData, AuthCodeRequest, AuthStorage } from './auth-storage.js';
|
|
5
|
+
export type { AuthProviderModule } from './auth-module.js';
|
|
6
|
+
export { nullAuthStorage, BaseAuthStorage } from './auth-storage.js';
|
|
7
|
+
export { nullAuthModule, BaseAuthModule } from './auth-module.js';
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Application, Request, Response } from 'express';
|
|
8
8
|
import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
9
|
+
import type { AuthProviderModule } from './auth-module.js';
|
|
10
|
+
import type { AuthIdentifier, AuthStorage, AuthTokenData, AuthTokenQuery } from './auth-storage.js';
|
|
9
11
|
export type { Application, Request, Response, NextFunction, Router } from 'express';
|
|
10
12
|
export type { Multer } from 'multer';
|
|
11
13
|
export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
@@ -104,39 +106,42 @@ export declare class ApiServer {
|
|
|
104
106
|
app: Application;
|
|
105
107
|
currReq: ApiRequest | null;
|
|
106
108
|
readonly config: ApiServerConf;
|
|
109
|
+
private storageAdapter;
|
|
110
|
+
private moduleAdapter;
|
|
107
111
|
constructor(config?: Partial<ApiServerConf>);
|
|
112
|
+
authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
113
|
+
/**
|
|
114
|
+
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
115
|
+
*/
|
|
116
|
+
useAuthStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
117
|
+
authModule<UserRow, SafeUser>(module: AuthProviderModule<UserRow, SafeUser>): this;
|
|
118
|
+
/**
|
|
119
|
+
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
120
|
+
*/
|
|
121
|
+
useAuthModule<UserRow, SafeUser>(module: AuthProviderModule<UserRow, SafeUser>): this;
|
|
122
|
+
getAuthStorage(): AuthStorage<any, any>;
|
|
123
|
+
getAuthModule(): AuthProviderModule<any, any>;
|
|
108
124
|
jwtSign(payload: any, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
|
|
109
125
|
jwtVerify<T>(token: string, secret: string, options?: VerifyOptions): JwtVerifyResult<T>;
|
|
110
126
|
jwtDecode<T>(token: string, options?: jwt.DecodeOptions): JwtDecodeResult<T>;
|
|
111
127
|
getApiKey<T = ApiKey>(token: string): Promise<T | null>;
|
|
112
|
-
getUser(uid:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
getToken(params: {
|
|
123
|
-
accessToken?: string;
|
|
124
|
-
refreshToken?: string;
|
|
125
|
-
userId?: unknown;
|
|
126
|
-
}): Promise<unknown>;
|
|
127
|
-
updateToken(params: {
|
|
128
|
+
getUser(uid: AuthIdentifier): Promise<unknown>;
|
|
129
|
+
getUserPasswordHash(user: unknown): string;
|
|
130
|
+
getUserId(user: unknown): AuthIdentifier;
|
|
131
|
+
authenticateUser(params: {
|
|
132
|
+
login: string;
|
|
133
|
+
password: string;
|
|
134
|
+
}): Promise<boolean>;
|
|
135
|
+
storeToken(data: AuthTokenData): Promise<void>;
|
|
136
|
+
getToken(query: AuthTokenQuery): Promise<AuthTokenData | null>;
|
|
137
|
+
updateToken(updates: {
|
|
128
138
|
accessToken: string;
|
|
129
139
|
refreshToken: string;
|
|
130
140
|
expires?: Date;
|
|
141
|
+
clientId?: string;
|
|
142
|
+
scope?: string[];
|
|
131
143
|
}): Promise<boolean>;
|
|
132
|
-
deleteToken(
|
|
133
|
-
refreshToken?: string;
|
|
134
|
-
accessToken?: string;
|
|
135
|
-
userId?: unknown;
|
|
136
|
-
domain?: string;
|
|
137
|
-
fingerprint?: string;
|
|
138
|
-
label?: string;
|
|
139
|
-
}): Promise<number>;
|
|
144
|
+
deleteToken(query: AuthTokenQuery): Promise<number>;
|
|
140
145
|
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
141
146
|
filterUser<T = any, U = any>(fullUser: T): U;
|
|
142
147
|
guessExceptionText(error: any, defMsg?: string): string;
|
|
@@ -146,6 +151,8 @@ export declare class ApiServer {
|
|
|
146
151
|
private verifyJWT;
|
|
147
152
|
private authenticate;
|
|
148
153
|
private handle_request;
|
|
154
|
+
getClientIp(req: RequestWithStuff): string | null;
|
|
155
|
+
getClientIpChain(req: RequestWithStuff): string[];
|
|
149
156
|
api<T extends ApiModule<any>>(module: T): this;
|
|
150
157
|
dumpRequest(apiReq: ApiRequest): void;
|
|
151
158
|
}
|
|
@@ -9,6 +9,8 @@ import cors from 'cors';
|
|
|
9
9
|
import express from 'express';
|
|
10
10
|
import jwt from 'jsonwebtoken';
|
|
11
11
|
import multer from 'multer';
|
|
12
|
+
import { nullAuthModule } from './auth-module.js';
|
|
13
|
+
import { nullAuthStorage } from './auth-storage.js';
|
|
12
14
|
export class ApiModule {
|
|
13
15
|
constructor(opts = {}) {
|
|
14
16
|
this.mountpath = '';
|
|
@@ -55,6 +57,76 @@ function hydrateGetBody(req) {
|
|
|
55
57
|
}
|
|
56
58
|
req.body = { ...query, ...body };
|
|
57
59
|
}
|
|
60
|
+
function normalizeIpAddress(candidate) {
|
|
61
|
+
let value = candidate.trim();
|
|
62
|
+
if (!value) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
value = value.replace(/^"+|"+$/g, '').replace(/^'+|'+$/g, '');
|
|
66
|
+
if (value.startsWith('::ffff:')) {
|
|
67
|
+
value = value.slice(7);
|
|
68
|
+
}
|
|
69
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
70
|
+
value = value.slice(1, -1);
|
|
71
|
+
}
|
|
72
|
+
const firstColon = value.indexOf(':');
|
|
73
|
+
const lastColon = value.lastIndexOf(':');
|
|
74
|
+
if (firstColon !== -1 && firstColon === lastColon) {
|
|
75
|
+
const maybePort = value.slice(lastColon + 1);
|
|
76
|
+
if (/^\d+$/.test(maybePort)) {
|
|
77
|
+
value = value.slice(0, lastColon);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
value = value.trim();
|
|
81
|
+
return value || null;
|
|
82
|
+
}
|
|
83
|
+
function extractForwardedFor(header) {
|
|
84
|
+
if (!header) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const values = Array.isArray(header) ? header : [header];
|
|
88
|
+
const ips = [];
|
|
89
|
+
for (const entry of values) {
|
|
90
|
+
for (const part of entry.split(',')) {
|
|
91
|
+
const normalized = normalizeIpAddress(part);
|
|
92
|
+
if (normalized) {
|
|
93
|
+
ips.push(normalized);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return ips;
|
|
98
|
+
}
|
|
99
|
+
function extractForwardedHeader(header) {
|
|
100
|
+
if (!header) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const values = Array.isArray(header) ? header : [header];
|
|
104
|
+
const ips = [];
|
|
105
|
+
for (const entry of values) {
|
|
106
|
+
for (const part of entry.split(',')) {
|
|
107
|
+
const match = part.match(/for=([^;]+)/i);
|
|
108
|
+
if (match) {
|
|
109
|
+
const normalized = normalizeIpAddress(match[1]);
|
|
110
|
+
if (normalized) {
|
|
111
|
+
ips.push(normalized);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return ips;
|
|
117
|
+
}
|
|
118
|
+
function isLoopbackAddress(ip) {
|
|
119
|
+
if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (ip === '0.0.0.0' || ip === '127.0.0.1') {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (ip.startsWith('127.')) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
58
130
|
export class ApiError extends Error {
|
|
59
131
|
constructor({ code, message, data, errors }) {
|
|
60
132
|
const msg = guess_exception_text(message, '[Unknown error (null/undefined)]');
|
|
@@ -90,6 +162,8 @@ export class ApiServer {
|
|
|
90
162
|
constructor(config = {}) {
|
|
91
163
|
this.currReq = null;
|
|
92
164
|
this.config = fillConfig(config);
|
|
165
|
+
this.storageAdapter = nullAuthStorage;
|
|
166
|
+
this.moduleAdapter = nullAuthModule;
|
|
93
167
|
this.app = express();
|
|
94
168
|
if (config.uploadPath) {
|
|
95
169
|
const upload = multer({ dest: config.uploadPath });
|
|
@@ -98,6 +172,32 @@ export class ApiServer {
|
|
|
98
172
|
this.middlewares();
|
|
99
173
|
// addSwaggerUi(this.app);
|
|
100
174
|
}
|
|
175
|
+
authStorage(storage) {
|
|
176
|
+
this.storageAdapter = storage;
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
181
|
+
*/
|
|
182
|
+
useAuthStorage(storage) {
|
|
183
|
+
return this.authStorage(storage);
|
|
184
|
+
}
|
|
185
|
+
authModule(module) {
|
|
186
|
+
this.moduleAdapter = module;
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
191
|
+
*/
|
|
192
|
+
useAuthModule(module) {
|
|
193
|
+
return this.authModule(module);
|
|
194
|
+
}
|
|
195
|
+
getAuthStorage() {
|
|
196
|
+
return this.storageAdapter;
|
|
197
|
+
}
|
|
198
|
+
getAuthModule() {
|
|
199
|
+
return this.moduleAdapter;
|
|
200
|
+
}
|
|
101
201
|
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
102
202
|
options || (options = {});
|
|
103
203
|
const opts = { ...options, expiresIn: expiresInSeconds };
|
|
@@ -166,36 +266,63 @@ export class ApiServer {
|
|
|
166
266
|
}
|
|
167
267
|
}
|
|
168
268
|
async getApiKey(token) {
|
|
269
|
+
void token;
|
|
169
270
|
return null;
|
|
170
271
|
}
|
|
171
272
|
async getUser(uid) {
|
|
172
|
-
|
|
273
|
+
return this.storageAdapter.getUser(uid);
|
|
274
|
+
}
|
|
275
|
+
getUserPasswordHash(user) {
|
|
276
|
+
return this.storageAdapter.getUserPasswordHash(user);
|
|
277
|
+
}
|
|
278
|
+
getUserId(user) {
|
|
279
|
+
return this.storageAdapter.getUserId(user);
|
|
173
280
|
}
|
|
174
281
|
async authenticateUser(params) {
|
|
175
|
-
|
|
282
|
+
if (!params?.login || !params?.password) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const user = await this.getUser(params.login);
|
|
286
|
+
if (!user) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const hash = this.storageAdapter.getUserPasswordHash(user);
|
|
290
|
+
return this.verifyPassword(params.password, hash);
|
|
176
291
|
}
|
|
177
|
-
async storeToken(
|
|
178
|
-
|
|
292
|
+
async storeToken(data) {
|
|
293
|
+
await this.storageAdapter.storeToken(data);
|
|
179
294
|
}
|
|
180
|
-
async getToken(
|
|
181
|
-
|
|
295
|
+
async getToken(query) {
|
|
296
|
+
return this.storageAdapter.getToken(query);
|
|
182
297
|
}
|
|
183
|
-
async updateToken(
|
|
184
|
-
|
|
298
|
+
async updateToken(updates) {
|
|
299
|
+
if (typeof this.storageAdapter.updateToken !== 'function') {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return this.storageAdapter.updateToken({
|
|
303
|
+
refreshToken: updates.refreshToken,
|
|
304
|
+
access: updates.accessToken,
|
|
305
|
+
expires: updates.expires,
|
|
306
|
+
clientId: updates.clientId,
|
|
307
|
+
scope: updates.scope
|
|
308
|
+
});
|
|
185
309
|
}
|
|
186
|
-
async deleteToken(
|
|
187
|
-
|
|
310
|
+
async deleteToken(query) {
|
|
311
|
+
return this.storageAdapter.deleteToken(query);
|
|
188
312
|
}
|
|
189
313
|
async verifyPassword(password, hash) {
|
|
190
|
-
|
|
314
|
+
return this.storageAdapter.verifyPassword(password, hash);
|
|
191
315
|
}
|
|
192
316
|
filterUser(fullUser) {
|
|
193
|
-
return fullUser;
|
|
317
|
+
return this.storageAdapter.filterUser(fullUser);
|
|
194
318
|
}
|
|
195
319
|
guessExceptionText(error, defMsg = 'Unkown Error') {
|
|
196
320
|
return guess_exception_text(error, defMsg);
|
|
197
321
|
}
|
|
198
|
-
async authorize(apiReq, requiredClass) {
|
|
322
|
+
async authorize(apiReq, requiredClass) {
|
|
323
|
+
void apiReq;
|
|
324
|
+
void requiredClass;
|
|
325
|
+
}
|
|
199
326
|
middlewares() {
|
|
200
327
|
this.app.use(express.json());
|
|
201
328
|
this.app.use(cookieParser());
|
|
@@ -312,6 +439,7 @@ export class ApiServer {
|
|
|
312
439
|
}
|
|
313
440
|
handle_request(handler, auth) {
|
|
314
441
|
return async (req, res, next) => {
|
|
442
|
+
void next;
|
|
315
443
|
try {
|
|
316
444
|
const apiReq = (this.currReq = {
|
|
317
445
|
server: this,
|
|
@@ -351,9 +479,62 @@ export class ApiServer {
|
|
|
351
479
|
}
|
|
352
480
|
};
|
|
353
481
|
}
|
|
482
|
+
getClientIp(req) {
|
|
483
|
+
const chain = this.getClientIpChain(req);
|
|
484
|
+
for (const ip of chain) {
|
|
485
|
+
if (!isLoopbackAddress(ip)) {
|
|
486
|
+
return ip;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return chain[0] ?? null;
|
|
490
|
+
}
|
|
491
|
+
getClientIpChain(req) {
|
|
492
|
+
const seen = new Set();
|
|
493
|
+
const result = [];
|
|
494
|
+
const pushNormalized = (ip) => {
|
|
495
|
+
if (!ip || seen.has(ip)) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
seen.add(ip);
|
|
499
|
+
result.push(ip);
|
|
500
|
+
};
|
|
501
|
+
for (const ip of extractForwardedFor(req.headers['x-forwarded-for'])) {
|
|
502
|
+
pushNormalized(ip);
|
|
503
|
+
}
|
|
504
|
+
for (const ip of extractForwardedHeader(req.headers['forwarded'])) {
|
|
505
|
+
pushNormalized(ip);
|
|
506
|
+
}
|
|
507
|
+
const realIp = req.headers['x-real-ip'];
|
|
508
|
+
if (Array.isArray(realIp)) {
|
|
509
|
+
realIp.forEach((value) => pushNormalized(normalizeIpAddress(value)));
|
|
510
|
+
}
|
|
511
|
+
else if (typeof realIp === 'string') {
|
|
512
|
+
pushNormalized(normalizeIpAddress(realIp));
|
|
513
|
+
}
|
|
514
|
+
if (Array.isArray(req.ips)) {
|
|
515
|
+
for (const ip of req.ips) {
|
|
516
|
+
pushNormalized(normalizeIpAddress(ip));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (typeof req.ip === 'string') {
|
|
520
|
+
pushNormalized(normalizeIpAddress(req.ip));
|
|
521
|
+
}
|
|
522
|
+
const socketAddress = req.socket?.remoteAddress;
|
|
523
|
+
if (typeof socketAddress === 'string') {
|
|
524
|
+
pushNormalized(normalizeIpAddress(socketAddress));
|
|
525
|
+
}
|
|
526
|
+
const connectionAddress = req.connection?.remoteAddress;
|
|
527
|
+
if (typeof connectionAddress === 'string') {
|
|
528
|
+
pushNormalized(normalizeIpAddress(connectionAddress));
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
354
532
|
api(module) {
|
|
355
533
|
const router = express.Router();
|
|
356
534
|
module.server = this;
|
|
535
|
+
if (module?.moduleType === 'auth') {
|
|
536
|
+
this.authModule(module);
|
|
537
|
+
}
|
|
357
538
|
module.checkConfig();
|
|
358
539
|
const base = this.config.apiBasePath ?? '/api';
|
|
359
540
|
const ns = module.namespace;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApiRequest } from './api-server-base.js';
|
|
2
|
+
import type { AuthTokenMetadata, AuthTokenPair } from './auth-storage.js';
|
|
3
|
+
export interface AuthProviderModule<UserRow, SafeUser> {
|
|
4
|
+
readonly moduleType: 'auth';
|
|
5
|
+
readonly namespace: string;
|
|
6
|
+
issueTokens(apiReq: ApiRequest, user: UserRow, metadata?: AuthTokenMetadata & {
|
|
7
|
+
expires?: Date;
|
|
8
|
+
}): Promise<AuthTokenPair>;
|
|
9
|
+
}
|
|
10
|
+
export declare abstract class BaseAuthModule implements AuthProviderModule<unknown, unknown> {
|
|
11
|
+
readonly moduleType: "auth";
|
|
12
|
+
abstract readonly namespace: string;
|
|
13
|
+
issueTokens(apiReq: ApiRequest, user: unknown, metadata?: AuthTokenMetadata & {
|
|
14
|
+
expires?: Date;
|
|
15
|
+
}): Promise<AuthTokenPair>;
|
|
16
|
+
}
|
|
17
|
+
export declare const nullAuthModule: AuthProviderModule<unknown, unknown>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Handy base that you can extend when wiring a real auth module. Subclasses
|
|
2
|
+
// must provide their namespace. Methods throw by default so unimplemented
|
|
3
|
+
// hooks fail loudly.
|
|
4
|
+
export class BaseAuthModule {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.moduleType = 'auth';
|
|
7
|
+
}
|
|
8
|
+
// Override to mint tokens for the provided user and request context
|
|
9
|
+
async issueTokens(apiReq, user, metadata) {
|
|
10
|
+
void apiReq;
|
|
11
|
+
void user;
|
|
12
|
+
void metadata;
|
|
13
|
+
throw new Error('Auth module not configured');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
class NullAuthModule extends BaseAuthModule {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
this.namespace = '__null__';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const nullAuthModule = new NullAuthModule();
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export type AuthIdentifier = string | number;
|
|
2
|
+
export interface AuthTokenMetadata {
|
|
3
|
+
clientId?: string;
|
|
4
|
+
domain?: string;
|
|
5
|
+
fingerprint?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
scope?: string | string[];
|
|
8
|
+
}
|
|
9
|
+
export interface AuthTokenData extends AuthTokenMetadata {
|
|
10
|
+
access: string;
|
|
11
|
+
expires?: Date;
|
|
12
|
+
refresh: string;
|
|
13
|
+
userId: AuthIdentifier;
|
|
14
|
+
}
|
|
15
|
+
export interface AuthTokenQuery extends AuthTokenMetadata {
|
|
16
|
+
accessToken?: string;
|
|
17
|
+
refreshToken?: string;
|
|
18
|
+
userId?: AuthIdentifier;
|
|
19
|
+
}
|
|
20
|
+
export interface AuthTokenPair {
|
|
21
|
+
accessToken: string;
|
|
22
|
+
refreshToken: string;
|
|
23
|
+
}
|
|
24
|
+
export interface AuthTokenPayload extends AuthTokenMetadata {
|
|
25
|
+
exp?: number;
|
|
26
|
+
iat?: number;
|
|
27
|
+
uid: AuthIdentifier;
|
|
28
|
+
}
|
|
29
|
+
export interface PasskeyChallengeParams extends AuthTokenMetadata {
|
|
30
|
+
action: 'register' | 'authenticate';
|
|
31
|
+
login?: string;
|
|
32
|
+
userAgent?: string;
|
|
33
|
+
userId?: AuthIdentifier;
|
|
34
|
+
}
|
|
35
|
+
export interface PasskeyChallenge extends Record<string, unknown> {
|
|
36
|
+
challenge: string;
|
|
37
|
+
expiresAt?: string | number | Date;
|
|
38
|
+
userId?: AuthIdentifier;
|
|
39
|
+
}
|
|
40
|
+
export interface PasskeyVerificationParams extends AuthTokenMetadata {
|
|
41
|
+
expectedChallenge: string;
|
|
42
|
+
login?: string;
|
|
43
|
+
response: Record<string, unknown>;
|
|
44
|
+
userId?: AuthIdentifier;
|
|
45
|
+
}
|
|
46
|
+
export interface PasskeyVerificationResult extends Record<string, unknown> {
|
|
47
|
+
login?: string;
|
|
48
|
+
tokens?: AuthTokenPair;
|
|
49
|
+
userId?: AuthIdentifier;
|
|
50
|
+
verified: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface OAuthClient {
|
|
53
|
+
clientId: string;
|
|
54
|
+
clientSecret: string;
|
|
55
|
+
firstParty?: boolean;
|
|
56
|
+
metadata?: Record<string, unknown>;
|
|
57
|
+
name?: string;
|
|
58
|
+
redirectUris: string[];
|
|
59
|
+
scope?: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface AuthCodeData {
|
|
62
|
+
code: string;
|
|
63
|
+
clientId: string;
|
|
64
|
+
codeChallenge?: string;
|
|
65
|
+
codeChallengeMethod?: 'plain' | 'S256';
|
|
66
|
+
expiresAt: Date;
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
redirectUri: string;
|
|
69
|
+
scope: string[];
|
|
70
|
+
userId: AuthIdentifier;
|
|
71
|
+
}
|
|
72
|
+
export type AuthCodeRequest = Omit<AuthCodeData, 'code' | 'expiresAt'> & {
|
|
73
|
+
code?: string;
|
|
74
|
+
expiresInSeconds?: number;
|
|
75
|
+
};
|
|
76
|
+
export interface AuthStorage<UserRow, SafeUser> {
|
|
77
|
+
getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
|
|
78
|
+
getUserPasswordHash(user: UserRow): string;
|
|
79
|
+
getUserId(user: UserRow): AuthIdentifier;
|
|
80
|
+
filterUser(user: UserRow): SafeUser;
|
|
81
|
+
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
82
|
+
storeToken(data: AuthTokenData): Promise<void>;
|
|
83
|
+
getToken(query: AuthTokenQuery): Promise<AuthTokenData | null>;
|
|
84
|
+
deleteToken(query: AuthTokenQuery): Promise<number>;
|
|
85
|
+
updateToken?(updates: Partial<AuthTokenData> & {
|
|
86
|
+
refreshToken: string;
|
|
87
|
+
}): Promise<boolean>;
|
|
88
|
+
createPasskeyChallenge?(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
89
|
+
verifyPasskeyResponse?(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
90
|
+
getClient?(clientId: string): Promise<OAuthClient | null>;
|
|
91
|
+
verifyClientSecret?(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
92
|
+
createAuthCode?(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
93
|
+
consumeAuthCode?(code: string, clientId: string): Promise<AuthCodeData | null>;
|
|
94
|
+
}
|
|
95
|
+
export declare class BaseAuthStorage<UserRow = unknown, SafeUser = unknown> implements AuthStorage<UserRow, SafeUser> {
|
|
96
|
+
getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
|
|
97
|
+
getUserPasswordHash(user: UserRow): string;
|
|
98
|
+
getUserId(user: UserRow): AuthIdentifier;
|
|
99
|
+
filterUser(user: UserRow): SafeUser;
|
|
100
|
+
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
101
|
+
storeToken(data: AuthTokenData): Promise<void>;
|
|
102
|
+
getToken(query: AuthTokenQuery): Promise<AuthTokenData | null>;
|
|
103
|
+
deleteToken(query: AuthTokenQuery): Promise<number>;
|
|
104
|
+
updateToken(updates: Partial<AuthTokenData> & {
|
|
105
|
+
refreshToken: string;
|
|
106
|
+
}): Promise<boolean>;
|
|
107
|
+
createPasskeyChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
108
|
+
verifyPasskeyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
109
|
+
getClient(clientId: string): Promise<OAuthClient | null>;
|
|
110
|
+
verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
111
|
+
createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
112
|
+
consumeAuthCode(code: string, clientId: string): Promise<AuthCodeData | null>;
|
|
113
|
+
}
|
|
114
|
+
export declare const nullAuthStorage: AuthStorage<unknown, unknown>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Numeric database id or lookup string such as username/email.
|
|
2
|
+
// Handy base you can extend when wiring a real storage adapter. Every method
|
|
3
|
+
// throws by default so unimplemented hooks fail loudly.
|
|
4
|
+
export class BaseAuthStorage {
|
|
5
|
+
// Override to load a user record by identifier
|
|
6
|
+
async getUser(identifier) {
|
|
7
|
+
void identifier;
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
// Override to return the stored password hash for the user
|
|
11
|
+
getUserPasswordHash(user) {
|
|
12
|
+
void user;
|
|
13
|
+
throw new Error('Auth storage not configured');
|
|
14
|
+
}
|
|
15
|
+
// Override to expose the canonical user identifier
|
|
16
|
+
getUserId(user) {
|
|
17
|
+
void user;
|
|
18
|
+
throw new Error('Auth storage not configured');
|
|
19
|
+
}
|
|
20
|
+
// Override to strip sensitive fields from the user record
|
|
21
|
+
filterUser(user) {
|
|
22
|
+
return user;
|
|
23
|
+
}
|
|
24
|
+
// Override to validate a raw password against the stored hash
|
|
25
|
+
async verifyPassword(password, hash) {
|
|
26
|
+
void password;
|
|
27
|
+
void hash;
|
|
28
|
+
throw new Error('Auth storage not configured');
|
|
29
|
+
}
|
|
30
|
+
// Override to persist newly issued tokens
|
|
31
|
+
async storeToken(data) {
|
|
32
|
+
void data;
|
|
33
|
+
throw new Error('Auth storage not configured');
|
|
34
|
+
}
|
|
35
|
+
// Override to look up a stored token by query
|
|
36
|
+
async getToken(query) {
|
|
37
|
+
void query;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
// Override to remove stored tokens that match the query
|
|
41
|
+
async deleteToken(query) {
|
|
42
|
+
void query;
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
// Override to update metadata for an existing refresh token
|
|
46
|
+
async updateToken(updates) {
|
|
47
|
+
void updates;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Override to create a new passkey challenge record
|
|
51
|
+
async createPasskeyChallenge(params) {
|
|
52
|
+
void params;
|
|
53
|
+
throw new Error('Auth storage not configured');
|
|
54
|
+
}
|
|
55
|
+
// Override to verify an incoming WebAuthn response
|
|
56
|
+
async verifyPasskeyResponse(params) {
|
|
57
|
+
void params;
|
|
58
|
+
throw new Error('Auth storage not configured');
|
|
59
|
+
}
|
|
60
|
+
// Override to fetch an OAuth client by identifier
|
|
61
|
+
async getClient(clientId) {
|
|
62
|
+
void clientId;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
// Override to compare a provided client secret against storage
|
|
66
|
+
async verifyClientSecret(client, clientSecret) {
|
|
67
|
+
void client;
|
|
68
|
+
void clientSecret;
|
|
69
|
+
throw new Error('Auth storage not configured');
|
|
70
|
+
}
|
|
71
|
+
// Override to create a new authorization code entry
|
|
72
|
+
async createAuthCode(request) {
|
|
73
|
+
void request;
|
|
74
|
+
throw new Error('Auth storage not configured');
|
|
75
|
+
}
|
|
76
|
+
// Override to consume and invalidate an authorization code
|
|
77
|
+
async consumeAuthCode(code, clientId) {
|
|
78
|
+
void code;
|
|
79
|
+
void clientId;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export const nullAuthStorage = new BaseAuthStorage();
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { default as ApiServer } from './api-server-base.js';
|
|
2
2
|
export { ApiModule, ApiError } from './api-server-base.js';
|
|
3
3
|
export type { ApiErrorParams, ApiHandler, ApiKey, ApiServerConf, ApiRequest, ApiRoute, ApiAuthType, ApiAuthClass, ApiTokenData, RequestWithStuff } from './api-server-base.js';
|
|
4
|
+
export type { AuthIdentifier, AuthTokenMetadata, AuthTokenData, AuthTokenQuery, AuthTokenPair, AuthTokenPayload, PasskeyChallengeParams, PasskeyChallenge, PasskeyVerificationParams, PasskeyVerificationResult, OAuthClient, AuthCodeData, AuthCodeRequest, AuthStorage } from './auth-storage.js';
|
|
5
|
+
export type { AuthProviderModule } from './auth-module.js';
|
|
6
|
+
export { nullAuthStorage, BaseAuthStorage } from './auth-storage.js';
|
|
7
|
+
export { nullAuthModule, BaseAuthModule } from './auth-module.js';
|
package/dist/esm/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technomoron/api-server-base",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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": [
|