@technomoron/api-server-base 1.0.43 → 1.1.1
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 +32 -12
- package/dist/cjs/api-module.cjs +17 -0
- package/dist/cjs/api-module.d.ts +27 -0
- package/dist/cjs/api-server-base.cjs +55 -43
- package/dist/cjs/api-server-base.d.ts +27 -52
- package/dist/cjs/auth-module.cjs +25 -0
- package/dist/cjs/auth-module.d.ts +20 -0
- package/dist/cjs/auth-storage.cjs +87 -0
- package/dist/cjs/auth-storage.d.ts +114 -0
- package/dist/cjs/index.cjs +9 -2
- package/dist/cjs/index.d.ts +6 -1
- package/dist/esm/api-module.d.ts +27 -0
- package/dist/esm/api-module.js +13 -0
- package/dist/esm/api-server-base.d.ts +27 -52
- package/dist/esm/api-server-base.js +54 -42
- package/dist/esm/auth-module.d.ts +20 -0
- package/dist/esm/auth-module.js +21 -0
- package/dist/esm/auth-storage.d.ts +114 -0
- package/dist/esm/auth-storage.js +83 -0
- package/dist/esm/index.d.ts +6 -1
- package/dist/esm/index.js +4 -1
- package/package.json +1 -1
package/README.txt
CHANGED
|
@@ -27,18 +27,32 @@ All runtime dependencies and `@types/*` packages are bundled with the distributi
|
|
|
27
27
|
|
|
28
28
|
Quick Start
|
|
29
29
|
-----------
|
|
30
|
-
import { ApiServer, ApiModule, ApiError } from '@technomoron/api-server-base';
|
|
30
|
+
import { ApiServer, ApiModule, ApiError, BaseAuthStorage } from '@technomoron/api-server-base';
|
|
31
|
+
|
|
32
|
+
type DemoUser = { id: string; email: string; password: string };
|
|
33
|
+
|
|
34
|
+
class DemoStorage extends BaseAuthStorage<DemoUser, Omit<DemoUser, 'password'>> {
|
|
35
|
+
private readonly users = new Map<string, DemoUser>([
|
|
36
|
+
['1', { id: '1', email: 'demo@example.com', password: 'secret' }]
|
|
37
|
+
]);
|
|
31
38
|
|
|
32
|
-
class AppServer extends ApiServer {
|
|
33
39
|
async getUser(uid: unknown) {
|
|
34
|
-
return
|
|
40
|
+
return this.users.get(String(uid)) ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getUserPasswordHash(user: DemoUser) {
|
|
44
|
+
return user.password;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
filterUser(user:
|
|
38
|
-
|
|
47
|
+
filterUser(user: DemoUser) {
|
|
48
|
+
const { password: _password, ...safe } = user;
|
|
49
|
+
void _password;
|
|
50
|
+
return safe;
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
class AppServer extends ApiServer {}
|
|
55
|
+
|
|
42
56
|
class UserModule extends ApiModule<AppServer> {
|
|
43
57
|
constructor() {
|
|
44
58
|
super({ namespace: '/users' });
|
|
@@ -51,24 +65,30 @@ class UserModule extends ApiModule<AppServer> {
|
|
|
51
65
|
path: '/',
|
|
52
66
|
auth: { type: 'yes', req: 'any' },
|
|
53
67
|
handler: async ({ server, tokenData }) => {
|
|
54
|
-
const
|
|
68
|
+
const storage = server.getAuthStorage();
|
|
69
|
+
const user = tokenData ? await storage.getUser(tokenData.uid) : null;
|
|
55
70
|
if (!user) {
|
|
56
71
|
throw new ApiError({ code: 404, message: 'User not found' });
|
|
57
72
|
}
|
|
58
|
-
return [200,
|
|
73
|
+
return [200, storage.filterUser(user)];
|
|
59
74
|
},
|
|
60
75
|
},
|
|
61
76
|
];
|
|
62
77
|
}
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
const yourStorageAdapter = new DemoStorage();
|
|
81
|
+
|
|
65
82
|
const server = new AppServer({
|
|
66
83
|
apiPort: 3101,
|
|
67
84
|
apiHost: '127.0.0.1',
|
|
68
|
-
accessSecret: 'replace-me'
|
|
69
|
-
})
|
|
85
|
+
accessSecret: 'replace-me'
|
|
86
|
+
})
|
|
87
|
+
.authStorage(yourStorageAdapter)
|
|
88
|
+
.api(new UserModule())
|
|
89
|
+
.start();
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
Need a dedicated auth module as well? Chain `.authModule(...)` in the same spot.
|
|
72
92
|
|
|
73
93
|
Handlers must return a tuple: [statusCode], [statusCode, data], or [statusCode, data, message]. Throw ApiError for predictable failures.
|
|
74
94
|
|
|
@@ -111,8 +131,8 @@ Use getClientIp(req) to obtain the most likely client address, skipping loopback
|
|
|
111
131
|
|
|
112
132
|
Extending the Base Classes
|
|
113
133
|
--------------------------
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
Implement the AuthStorage contract (getUser, verifyPassword, storeToken, updateToken, etc.) to integrate with your persistence layer, then supply it via authStorage().
|
|
135
|
+
Use your storage adapter's filterUser helper to trim sensitive data before returning responses.
|
|
116
136
|
Provide your own authorize method to enforce role based access control using the ApiAuthClass enum.
|
|
117
137
|
Create feature modules by extending ApiModule. Use the optional checkConfig hook to validate prerequisites before routes mount.
|
|
118
138
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiModule = void 0;
|
|
4
|
+
class ApiModule {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
this.mountpath = '';
|
|
7
|
+
this.namespace = opts.namespace ?? this.constructor.defaultNamespace ?? '';
|
|
8
|
+
}
|
|
9
|
+
checkConfig() {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
defineRoutes() {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.ApiModule = ApiModule;
|
|
17
|
+
ApiModule.defaultNamespace = '';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ApiRequest } from './api-server-base.js';
|
|
2
|
+
export type ApiHandler = (apiReq: ApiRequest) => Promise<[number] | [number, any] | [number, any, string]>;
|
|
3
|
+
export type ApiAuthType = 'none' | 'maybe' | 'yes';
|
|
4
|
+
export type ApiAuthClass = 'any' | 'admin';
|
|
5
|
+
export interface ApiKey {
|
|
6
|
+
uid: unknown;
|
|
7
|
+
}
|
|
8
|
+
export type ApiRoute = {
|
|
9
|
+
method: 'get' | 'post' | 'put' | 'delete';
|
|
10
|
+
path: string;
|
|
11
|
+
handler: ApiHandler;
|
|
12
|
+
auth: {
|
|
13
|
+
type: ApiAuthType;
|
|
14
|
+
req: ApiAuthClass;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare class ApiModule<T> {
|
|
18
|
+
server: T;
|
|
19
|
+
namespace: string;
|
|
20
|
+
mountpath: string;
|
|
21
|
+
static defaultNamespace: string;
|
|
22
|
+
constructor(opts?: {
|
|
23
|
+
namespace?: string;
|
|
24
|
+
});
|
|
25
|
+
checkConfig(): boolean;
|
|
26
|
+
defineRoutes(): ApiRoute[];
|
|
27
|
+
}
|
|
@@ -15,20 +15,10 @@ 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
checkConfig() {
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
defineRoutes() {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
exports.ApiModule = ApiModule;
|
|
31
|
-
ApiModule.defaultNamespace = '';
|
|
18
|
+
const auth_module_js_1 = require("./auth-module.cjs");
|
|
19
|
+
const auth_storage_js_1 = require("./auth-storage.cjs");
|
|
20
|
+
var api_module_js_1 = require("./api-module.cjs");
|
|
21
|
+
Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_module_js_1.ApiModule; } });
|
|
32
22
|
function guess_exception_text(error, defMsg = 'Unknown Error') {
|
|
33
23
|
const msg = [];
|
|
34
24
|
if (typeof error === 'string' && error.trim() !== '') {
|
|
@@ -168,6 +158,8 @@ class ApiServer {
|
|
|
168
158
|
constructor(config = {}) {
|
|
169
159
|
this.currReq = null;
|
|
170
160
|
this.config = fillConfig(config);
|
|
161
|
+
this.storageAdapter = auth_storage_js_1.nullAuthStorage;
|
|
162
|
+
this.moduleAdapter = auth_module_js_1.nullAuthModule;
|
|
171
163
|
this.app = (0, express_1.default)();
|
|
172
164
|
if (config.uploadPath) {
|
|
173
165
|
const upload = (0, multer_1.default)({ dest: config.uploadPath });
|
|
@@ -176,6 +168,32 @@ class ApiServer {
|
|
|
176
168
|
this.middlewares();
|
|
177
169
|
// addSwaggerUi(this.app);
|
|
178
170
|
}
|
|
171
|
+
authStorage(storage) {
|
|
172
|
+
this.storageAdapter = storage;
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
177
|
+
*/
|
|
178
|
+
useAuthStorage(storage) {
|
|
179
|
+
return this.authStorage(storage);
|
|
180
|
+
}
|
|
181
|
+
authModule(module) {
|
|
182
|
+
this.moduleAdapter = module;
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
187
|
+
*/
|
|
188
|
+
useAuthModule(module) {
|
|
189
|
+
return this.authModule(module);
|
|
190
|
+
}
|
|
191
|
+
getAuthStorage() {
|
|
192
|
+
return this.storageAdapter;
|
|
193
|
+
}
|
|
194
|
+
getAuthModule() {
|
|
195
|
+
return this.moduleAdapter;
|
|
196
|
+
}
|
|
179
197
|
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
180
198
|
options || (options = {});
|
|
181
199
|
const opts = { ...options, expiresIn: expiresInSeconds };
|
|
@@ -247,37 +265,28 @@ class ApiServer {
|
|
|
247
265
|
void token;
|
|
248
266
|
return null;
|
|
249
267
|
}
|
|
250
|
-
async getUser(uid) {
|
|
251
|
-
void uid;
|
|
252
|
-
throw new Error('getUser() not implemented');
|
|
253
|
-
}
|
|
254
268
|
async authenticateUser(params) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
throw new Error('getToken() not implemented');
|
|
265
|
-
}
|
|
266
|
-
async updateToken(params) {
|
|
267
|
-
void params;
|
|
268
|
-
throw new Error('updateToken() not implemented');
|
|
269
|
-
}
|
|
270
|
-
async deleteToken(params) {
|
|
271
|
-
void params;
|
|
272
|
-
throw new Error('deleteToken() not implemented');
|
|
273
|
-
}
|
|
274
|
-
async verifyPassword(password, hash) {
|
|
275
|
-
void password;
|
|
276
|
-
void hash;
|
|
277
|
-
throw new Error('verifyPassword() not implemented');
|
|
269
|
+
if (!params?.login || !params?.password) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const user = await this.storageAdapter.getUser(params.login);
|
|
273
|
+
if (!user) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const hash = this.storageAdapter.getUserPasswordHash(user);
|
|
277
|
+
return this.storageAdapter.verifyPassword(params.password, hash);
|
|
278
278
|
}
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
async updateToken(updates) {
|
|
280
|
+
if (typeof this.storageAdapter.updateToken !== 'function') {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
return this.storageAdapter.updateToken({
|
|
284
|
+
refreshToken: updates.refreshToken,
|
|
285
|
+
access: updates.accessToken,
|
|
286
|
+
expires: updates.expires,
|
|
287
|
+
clientId: updates.clientId,
|
|
288
|
+
scope: updates.scope
|
|
289
|
+
});
|
|
281
290
|
}
|
|
282
291
|
guessExceptionText(error, defMsg = 'Unkown Error') {
|
|
283
292
|
return guess_exception_text(error, defMsg);
|
|
@@ -495,6 +504,9 @@ class ApiServer {
|
|
|
495
504
|
api(module) {
|
|
496
505
|
const router = express_1.default.Router();
|
|
497
506
|
module.server = this;
|
|
507
|
+
if (module?.moduleType === 'auth') {
|
|
508
|
+
this.authModule(module);
|
|
509
|
+
}
|
|
498
510
|
module.checkConfig();
|
|
499
511
|
const base = this.config.apiBasePath ?? '/api';
|
|
500
512
|
const ns = module.namespace;
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Application, Request, Response } from 'express';
|
|
8
8
|
import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
9
|
+
import { ApiModule } from './api-module.js';
|
|
10
|
+
import type { ApiAuthClass, ApiKey } from './api-module.js';
|
|
11
|
+
import type { AuthProviderModule } from './auth-module.js';
|
|
12
|
+
import type { AuthStorage } from './auth-storage.js';
|
|
9
13
|
export type { Application, Request, Response, NextFunction, Router } from 'express';
|
|
10
14
|
export type { Multer } from 'multer';
|
|
11
15
|
export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
@@ -43,32 +47,8 @@ export interface ApiRequest {
|
|
|
43
47
|
tokenData?: ApiTokenData | null;
|
|
44
48
|
token?: string;
|
|
45
49
|
}
|
|
46
|
-
export
|
|
47
|
-
export type ApiAuthType
|
|
48
|
-
export type ApiAuthClass = 'any' | 'admin';
|
|
49
|
-
export interface ApiKey {
|
|
50
|
-
uid: unknown;
|
|
51
|
-
}
|
|
52
|
-
export type ApiRoute = {
|
|
53
|
-
method: 'get' | 'post' | 'put' | 'delete';
|
|
54
|
-
path: string;
|
|
55
|
-
handler: ApiHandler;
|
|
56
|
-
auth: {
|
|
57
|
-
type: ApiAuthType;
|
|
58
|
-
req: ApiAuthClass;
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
export declare class ApiModule<T> {
|
|
62
|
-
server: T;
|
|
63
|
-
namespace: string;
|
|
64
|
-
mountpath: string;
|
|
65
|
-
static defaultNamespace: string;
|
|
66
|
-
constructor(opts?: {
|
|
67
|
-
namespace?: string;
|
|
68
|
-
});
|
|
69
|
-
checkConfig(): boolean;
|
|
70
|
-
defineRoutes(): ApiRoute[];
|
|
71
|
-
}
|
|
50
|
+
export { ApiModule } from './api-module.js';
|
|
51
|
+
export type { ApiHandler, ApiAuthType, ApiAuthClass, ApiRoute, ApiKey } from './api-module.js';
|
|
72
52
|
export interface ApiErrorParams {
|
|
73
53
|
code?: number;
|
|
74
54
|
message?: any;
|
|
@@ -104,41 +84,36 @@ export declare class ApiServer {
|
|
|
104
84
|
app: Application;
|
|
105
85
|
currReq: ApiRequest | null;
|
|
106
86
|
readonly config: ApiServerConf;
|
|
87
|
+
private storageAdapter;
|
|
88
|
+
private moduleAdapter;
|
|
107
89
|
constructor(config?: Partial<ApiServerConf>);
|
|
90
|
+
authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
91
|
+
/**
|
|
92
|
+
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
93
|
+
*/
|
|
94
|
+
useAuthStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
95
|
+
authModule<UserRow>(module: AuthProviderModule<UserRow>): this;
|
|
96
|
+
/**
|
|
97
|
+
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
98
|
+
*/
|
|
99
|
+
useAuthModule<UserRow>(module: AuthProviderModule<UserRow>): this;
|
|
100
|
+
getAuthStorage(): AuthStorage<any, any>;
|
|
101
|
+
getAuthModule(): AuthProviderModule<any>;
|
|
108
102
|
jwtSign(payload: any, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
|
|
109
103
|
jwtVerify<T>(token: string, secret: string, options?: VerifyOptions): JwtVerifyResult<T>;
|
|
110
104
|
jwtDecode<T>(token: string, options?: jwt.DecodeOptions): JwtDecodeResult<T>;
|
|
111
105
|
getApiKey<T = ApiKey>(token: string): Promise<T | null>;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
userId: unknown;
|
|
118
|
-
domain?: string;
|
|
119
|
-
fingerprint?: string;
|
|
120
|
-
label?: string;
|
|
121
|
-
}): Promise<void>;
|
|
122
|
-
getToken(params: {
|
|
123
|
-
accessToken?: string;
|
|
124
|
-
refreshToken?: string;
|
|
125
|
-
userId?: unknown;
|
|
126
|
-
}): Promise<unknown>;
|
|
127
|
-
updateToken(params: {
|
|
106
|
+
authenticateUser(params: {
|
|
107
|
+
login: string;
|
|
108
|
+
password: string;
|
|
109
|
+
}): Promise<boolean>;
|
|
110
|
+
updateToken(updates: {
|
|
128
111
|
accessToken: string;
|
|
129
112
|
refreshToken: string;
|
|
130
113
|
expires?: Date;
|
|
114
|
+
clientId?: string;
|
|
115
|
+
scope?: string[];
|
|
131
116
|
}): Promise<boolean>;
|
|
132
|
-
deleteToken(params: {
|
|
133
|
-
refreshToken?: string;
|
|
134
|
-
accessToken?: string;
|
|
135
|
-
userId?: unknown;
|
|
136
|
-
domain?: string;
|
|
137
|
-
fingerprint?: string;
|
|
138
|
-
label?: string;
|
|
139
|
-
}): Promise<number>;
|
|
140
|
-
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
141
|
-
filterUser<T = any, U = any>(fullUser: T): U;
|
|
142
117
|
guessExceptionText(error: any, defMsg?: string): string;
|
|
143
118
|
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|
|
144
119
|
private middlewares;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nullAuthModule = exports.BaseAuthModule = void 0;
|
|
4
|
+
const api_module_js_1 = require("./api-module.cjs");
|
|
5
|
+
// Handy base that you can extend when wiring a real auth module. Subclasses
|
|
6
|
+
// must supply a namespace via the constructor and implement token issuance.
|
|
7
|
+
class BaseAuthModule extends api_module_js_1.ApiModule {
|
|
8
|
+
constructor(opts) {
|
|
9
|
+
super(opts);
|
|
10
|
+
this.moduleType = 'auth';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.BaseAuthModule = BaseAuthModule;
|
|
14
|
+
class NullAuthModule extends BaseAuthModule {
|
|
15
|
+
constructor() {
|
|
16
|
+
super({ namespace: '__null__' });
|
|
17
|
+
}
|
|
18
|
+
async issueTokens(apiReq, user, metadata) {
|
|
19
|
+
void apiReq;
|
|
20
|
+
void user;
|
|
21
|
+
void metadata;
|
|
22
|
+
throw new Error('Auth module not configured. Inject a real auth module before issuing tokens.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.nullAuthModule = new NullAuthModule();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ApiModule } from './api-module.js';
|
|
2
|
+
import type { ApiRequest } from './api-server-base.js';
|
|
3
|
+
import type { AuthTokenMetadata, AuthTokenPair } from './auth-storage.js';
|
|
4
|
+
export interface AuthProviderModule<UserRow = unknown> {
|
|
5
|
+
readonly moduleType: 'auth';
|
|
6
|
+
readonly namespace: string;
|
|
7
|
+
issueTokens(apiReq: ApiRequest, user: UserRow, metadata?: AuthTokenMetadata & {
|
|
8
|
+
expires?: Date;
|
|
9
|
+
}): Promise<AuthTokenPair>;
|
|
10
|
+
}
|
|
11
|
+
export declare abstract class BaseAuthModule<UserRow = unknown> extends ApiModule<any> implements AuthProviderModule<UserRow> {
|
|
12
|
+
readonly moduleType: "auth";
|
|
13
|
+
protected constructor(opts: {
|
|
14
|
+
namespace: string;
|
|
15
|
+
});
|
|
16
|
+
abstract issueTokens(apiReq: ApiRequest, user: UserRow, metadata?: AuthTokenMetadata & {
|
|
17
|
+
expires?: Date;
|
|
18
|
+
}): Promise<AuthTokenPair>;
|
|
19
|
+
}
|
|
20
|
+
export declare const nullAuthModule: AuthProviderModule<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,16 @@ 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.
|
|
6
|
+
exports.BaseAuthModule = exports.nullAuthModule = exports.BaseAuthStorage = exports.nullAuthStorage = exports.ApiModule = exports.ApiError = 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
|
-
Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_server_base_js_2.ApiModule; } });
|
|
11
10
|
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return api_server_base_js_2.ApiError; } });
|
|
11
|
+
var api_module_js_1 = require("./api-module.cjs");
|
|
12
|
+
Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_module_js_1.ApiModule; } });
|
|
13
|
+
var auth_storage_js_1 = require("./auth-storage.cjs");
|
|
14
|
+
Object.defineProperty(exports, "nullAuthStorage", { enumerable: true, get: function () { return auth_storage_js_1.nullAuthStorage; } });
|
|
15
|
+
Object.defineProperty(exports, "BaseAuthStorage", { enumerable: true, get: function () { return auth_storage_js_1.BaseAuthStorage; } });
|
|
16
|
+
var auth_module_js_1 = require("./auth-module.cjs");
|
|
17
|
+
Object.defineProperty(exports, "nullAuthModule", { enumerable: true, get: function () { return auth_module_js_1.nullAuthModule; } });
|
|
18
|
+
Object.defineProperty(exports, "BaseAuthModule", { enumerable: true, get: function () { return auth_module_js_1.BaseAuthModule; } });
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export { default as ApiServer } from './api-server-base.js';
|
|
2
|
-
export {
|
|
2
|
+
export { ApiError } from './api-server-base.js';
|
|
3
|
+
export { ApiModule } from './api-module.js';
|
|
3
4
|
export type { ApiErrorParams, ApiHandler, ApiKey, ApiServerConf, ApiRequest, ApiRoute, ApiAuthType, ApiAuthClass, ApiTokenData, RequestWithStuff } from './api-server-base.js';
|
|
5
|
+
export type { AuthIdentifier, AuthTokenMetadata, AuthTokenData, AuthTokenQuery, AuthTokenPair, AuthTokenPayload, PasskeyChallengeParams, PasskeyChallenge, PasskeyVerificationParams, PasskeyVerificationResult, OAuthClient, AuthCodeData, AuthCodeRequest, AuthStorage } from './auth-storage.js';
|
|
6
|
+
export type { AuthProviderModule } from './auth-module.js';
|
|
7
|
+
export { nullAuthStorage, BaseAuthStorage } from './auth-storage.js';
|
|
8
|
+
export { nullAuthModule, BaseAuthModule } from './auth-module.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ApiRequest } from './api-server-base.js';
|
|
2
|
+
export type ApiHandler = (apiReq: ApiRequest) => Promise<[number] | [number, any] | [number, any, string]>;
|
|
3
|
+
export type ApiAuthType = 'none' | 'maybe' | 'yes';
|
|
4
|
+
export type ApiAuthClass = 'any' | 'admin';
|
|
5
|
+
export interface ApiKey {
|
|
6
|
+
uid: unknown;
|
|
7
|
+
}
|
|
8
|
+
export type ApiRoute = {
|
|
9
|
+
method: 'get' | 'post' | 'put' | 'delete';
|
|
10
|
+
path: string;
|
|
11
|
+
handler: ApiHandler;
|
|
12
|
+
auth: {
|
|
13
|
+
type: ApiAuthType;
|
|
14
|
+
req: ApiAuthClass;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare class ApiModule<T> {
|
|
18
|
+
server: T;
|
|
19
|
+
namespace: string;
|
|
20
|
+
mountpath: string;
|
|
21
|
+
static defaultNamespace: string;
|
|
22
|
+
constructor(opts?: {
|
|
23
|
+
namespace?: string;
|
|
24
|
+
});
|
|
25
|
+
checkConfig(): boolean;
|
|
26
|
+
defineRoutes(): ApiRoute[];
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class ApiModule {
|
|
2
|
+
constructor(opts = {}) {
|
|
3
|
+
this.mountpath = '';
|
|
4
|
+
this.namespace = opts.namespace ?? this.constructor.defaultNamespace ?? '';
|
|
5
|
+
}
|
|
6
|
+
checkConfig() {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
defineRoutes() {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
ApiModule.defaultNamespace = '';
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Application, Request, Response } from 'express';
|
|
8
8
|
import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
9
|
+
import { ApiModule } from './api-module.js';
|
|
10
|
+
import type { ApiAuthClass, ApiKey } from './api-module.js';
|
|
11
|
+
import type { AuthProviderModule } from './auth-module.js';
|
|
12
|
+
import type { AuthStorage } from './auth-storage.js';
|
|
9
13
|
export type { Application, Request, Response, NextFunction, Router } from 'express';
|
|
10
14
|
export type { Multer } from 'multer';
|
|
11
15
|
export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
@@ -43,32 +47,8 @@ export interface ApiRequest {
|
|
|
43
47
|
tokenData?: ApiTokenData | null;
|
|
44
48
|
token?: string;
|
|
45
49
|
}
|
|
46
|
-
export
|
|
47
|
-
export type ApiAuthType
|
|
48
|
-
export type ApiAuthClass = 'any' | 'admin';
|
|
49
|
-
export interface ApiKey {
|
|
50
|
-
uid: unknown;
|
|
51
|
-
}
|
|
52
|
-
export type ApiRoute = {
|
|
53
|
-
method: 'get' | 'post' | 'put' | 'delete';
|
|
54
|
-
path: string;
|
|
55
|
-
handler: ApiHandler;
|
|
56
|
-
auth: {
|
|
57
|
-
type: ApiAuthType;
|
|
58
|
-
req: ApiAuthClass;
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
export declare class ApiModule<T> {
|
|
62
|
-
server: T;
|
|
63
|
-
namespace: string;
|
|
64
|
-
mountpath: string;
|
|
65
|
-
static defaultNamespace: string;
|
|
66
|
-
constructor(opts?: {
|
|
67
|
-
namespace?: string;
|
|
68
|
-
});
|
|
69
|
-
checkConfig(): boolean;
|
|
70
|
-
defineRoutes(): ApiRoute[];
|
|
71
|
-
}
|
|
50
|
+
export { ApiModule } from './api-module.js';
|
|
51
|
+
export type { ApiHandler, ApiAuthType, ApiAuthClass, ApiRoute, ApiKey } from './api-module.js';
|
|
72
52
|
export interface ApiErrorParams {
|
|
73
53
|
code?: number;
|
|
74
54
|
message?: any;
|
|
@@ -104,41 +84,36 @@ export declare class ApiServer {
|
|
|
104
84
|
app: Application;
|
|
105
85
|
currReq: ApiRequest | null;
|
|
106
86
|
readonly config: ApiServerConf;
|
|
87
|
+
private storageAdapter;
|
|
88
|
+
private moduleAdapter;
|
|
107
89
|
constructor(config?: Partial<ApiServerConf>);
|
|
90
|
+
authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
91
|
+
/**
|
|
92
|
+
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
93
|
+
*/
|
|
94
|
+
useAuthStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
95
|
+
authModule<UserRow>(module: AuthProviderModule<UserRow>): this;
|
|
96
|
+
/**
|
|
97
|
+
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
98
|
+
*/
|
|
99
|
+
useAuthModule<UserRow>(module: AuthProviderModule<UserRow>): this;
|
|
100
|
+
getAuthStorage(): AuthStorage<any, any>;
|
|
101
|
+
getAuthModule(): AuthProviderModule<any>;
|
|
108
102
|
jwtSign(payload: any, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
|
|
109
103
|
jwtVerify<T>(token: string, secret: string, options?: VerifyOptions): JwtVerifyResult<T>;
|
|
110
104
|
jwtDecode<T>(token: string, options?: jwt.DecodeOptions): JwtDecodeResult<T>;
|
|
111
105
|
getApiKey<T = ApiKey>(token: string): Promise<T | null>;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
userId: unknown;
|
|
118
|
-
domain?: string;
|
|
119
|
-
fingerprint?: string;
|
|
120
|
-
label?: string;
|
|
121
|
-
}): Promise<void>;
|
|
122
|
-
getToken(params: {
|
|
123
|
-
accessToken?: string;
|
|
124
|
-
refreshToken?: string;
|
|
125
|
-
userId?: unknown;
|
|
126
|
-
}): Promise<unknown>;
|
|
127
|
-
updateToken(params: {
|
|
106
|
+
authenticateUser(params: {
|
|
107
|
+
login: string;
|
|
108
|
+
password: string;
|
|
109
|
+
}): Promise<boolean>;
|
|
110
|
+
updateToken(updates: {
|
|
128
111
|
accessToken: string;
|
|
129
112
|
refreshToken: string;
|
|
130
113
|
expires?: Date;
|
|
114
|
+
clientId?: string;
|
|
115
|
+
scope?: string[];
|
|
131
116
|
}): Promise<boolean>;
|
|
132
|
-
deleteToken(params: {
|
|
133
|
-
refreshToken?: string;
|
|
134
|
-
accessToken?: string;
|
|
135
|
-
userId?: unknown;
|
|
136
|
-
domain?: string;
|
|
137
|
-
fingerprint?: string;
|
|
138
|
-
label?: string;
|
|
139
|
-
}): Promise<number>;
|
|
140
|
-
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
141
|
-
filterUser<T = any, U = any>(fullUser: T): U;
|
|
142
117
|
guessExceptionText(error: any, defMsg?: string): string;
|
|
143
118
|
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|
|
144
119
|
private middlewares;
|
|
@@ -9,19 +9,9 @@ import cors from 'cors';
|
|
|
9
9
|
import express from 'express';
|
|
10
10
|
import jwt from 'jsonwebtoken';
|
|
11
11
|
import multer from 'multer';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.namespace = opts.namespace ?? this.constructor.defaultNamespace ?? '';
|
|
16
|
-
}
|
|
17
|
-
checkConfig() {
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
defineRoutes() {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
ApiModule.defaultNamespace = '';
|
|
12
|
+
import { nullAuthModule } from './auth-module.js';
|
|
13
|
+
import { nullAuthStorage } from './auth-storage.js';
|
|
14
|
+
export { ApiModule } from './api-module.js';
|
|
25
15
|
function guess_exception_text(error, defMsg = 'Unknown Error') {
|
|
26
16
|
const msg = [];
|
|
27
17
|
if (typeof error === 'string' && error.trim() !== '') {
|
|
@@ -160,6 +150,8 @@ export class ApiServer {
|
|
|
160
150
|
constructor(config = {}) {
|
|
161
151
|
this.currReq = null;
|
|
162
152
|
this.config = fillConfig(config);
|
|
153
|
+
this.storageAdapter = nullAuthStorage;
|
|
154
|
+
this.moduleAdapter = nullAuthModule;
|
|
163
155
|
this.app = express();
|
|
164
156
|
if (config.uploadPath) {
|
|
165
157
|
const upload = multer({ dest: config.uploadPath });
|
|
@@ -168,6 +160,32 @@ export class ApiServer {
|
|
|
168
160
|
this.middlewares();
|
|
169
161
|
// addSwaggerUi(this.app);
|
|
170
162
|
}
|
|
163
|
+
authStorage(storage) {
|
|
164
|
+
this.storageAdapter = storage;
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* @deprecated Use {@link ApiServer.authStorage} instead.
|
|
169
|
+
*/
|
|
170
|
+
useAuthStorage(storage) {
|
|
171
|
+
return this.authStorage(storage);
|
|
172
|
+
}
|
|
173
|
+
authModule(module) {
|
|
174
|
+
this.moduleAdapter = module;
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* @deprecated Use {@link ApiServer.authModule} instead.
|
|
179
|
+
*/
|
|
180
|
+
useAuthModule(module) {
|
|
181
|
+
return this.authModule(module);
|
|
182
|
+
}
|
|
183
|
+
getAuthStorage() {
|
|
184
|
+
return this.storageAdapter;
|
|
185
|
+
}
|
|
186
|
+
getAuthModule() {
|
|
187
|
+
return this.moduleAdapter;
|
|
188
|
+
}
|
|
171
189
|
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
172
190
|
options || (options = {});
|
|
173
191
|
const opts = { ...options, expiresIn: expiresInSeconds };
|
|
@@ -239,37 +257,28 @@ export class ApiServer {
|
|
|
239
257
|
void token;
|
|
240
258
|
return null;
|
|
241
259
|
}
|
|
242
|
-
async getUser(uid) {
|
|
243
|
-
void uid;
|
|
244
|
-
throw new Error('getUser() not implemented');
|
|
245
|
-
}
|
|
246
260
|
async authenticateUser(params) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
throw new Error('getToken() not implemented');
|
|
257
|
-
}
|
|
258
|
-
async updateToken(params) {
|
|
259
|
-
void params;
|
|
260
|
-
throw new Error('updateToken() not implemented');
|
|
261
|
-
}
|
|
262
|
-
async deleteToken(params) {
|
|
263
|
-
void params;
|
|
264
|
-
throw new Error('deleteToken() not implemented');
|
|
265
|
-
}
|
|
266
|
-
async verifyPassword(password, hash) {
|
|
267
|
-
void password;
|
|
268
|
-
void hash;
|
|
269
|
-
throw new Error('verifyPassword() not implemented');
|
|
261
|
+
if (!params?.login || !params?.password) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const user = await this.storageAdapter.getUser(params.login);
|
|
265
|
+
if (!user) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const hash = this.storageAdapter.getUserPasswordHash(user);
|
|
269
|
+
return this.storageAdapter.verifyPassword(params.password, hash);
|
|
270
270
|
}
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
async updateToken(updates) {
|
|
272
|
+
if (typeof this.storageAdapter.updateToken !== 'function') {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
return this.storageAdapter.updateToken({
|
|
276
|
+
refreshToken: updates.refreshToken,
|
|
277
|
+
access: updates.accessToken,
|
|
278
|
+
expires: updates.expires,
|
|
279
|
+
clientId: updates.clientId,
|
|
280
|
+
scope: updates.scope
|
|
281
|
+
});
|
|
273
282
|
}
|
|
274
283
|
guessExceptionText(error, defMsg = 'Unkown Error') {
|
|
275
284
|
return guess_exception_text(error, defMsg);
|
|
@@ -487,6 +496,9 @@ export class ApiServer {
|
|
|
487
496
|
api(module) {
|
|
488
497
|
const router = express.Router();
|
|
489
498
|
module.server = this;
|
|
499
|
+
if (module?.moduleType === 'auth') {
|
|
500
|
+
this.authModule(module);
|
|
501
|
+
}
|
|
490
502
|
module.checkConfig();
|
|
491
503
|
const base = this.config.apiBasePath ?? '/api';
|
|
492
504
|
const ns = module.namespace;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ApiModule } from './api-module.js';
|
|
2
|
+
import type { ApiRequest } from './api-server-base.js';
|
|
3
|
+
import type { AuthTokenMetadata, AuthTokenPair } from './auth-storage.js';
|
|
4
|
+
export interface AuthProviderModule<UserRow = unknown> {
|
|
5
|
+
readonly moduleType: 'auth';
|
|
6
|
+
readonly namespace: string;
|
|
7
|
+
issueTokens(apiReq: ApiRequest, user: UserRow, metadata?: AuthTokenMetadata & {
|
|
8
|
+
expires?: Date;
|
|
9
|
+
}): Promise<AuthTokenPair>;
|
|
10
|
+
}
|
|
11
|
+
export declare abstract class BaseAuthModule<UserRow = unknown> extends ApiModule<any> implements AuthProviderModule<UserRow> {
|
|
12
|
+
readonly moduleType: "auth";
|
|
13
|
+
protected constructor(opts: {
|
|
14
|
+
namespace: string;
|
|
15
|
+
});
|
|
16
|
+
abstract issueTokens(apiReq: ApiRequest, user: UserRow, metadata?: AuthTokenMetadata & {
|
|
17
|
+
expires?: Date;
|
|
18
|
+
}): Promise<AuthTokenPair>;
|
|
19
|
+
}
|
|
20
|
+
export declare const nullAuthModule: AuthProviderModule<unknown>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ApiModule } from './api-module.js';
|
|
2
|
+
// Handy base that you can extend when wiring a real auth module. Subclasses
|
|
3
|
+
// must supply a namespace via the constructor and implement token issuance.
|
|
4
|
+
export class BaseAuthModule extends ApiModule {
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
super(opts);
|
|
7
|
+
this.moduleType = 'auth';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
class NullAuthModule extends BaseAuthModule {
|
|
11
|
+
constructor() {
|
|
12
|
+
super({ namespace: '__null__' });
|
|
13
|
+
}
|
|
14
|
+
async issueTokens(apiReq, user, metadata) {
|
|
15
|
+
void apiReq;
|
|
16
|
+
void user;
|
|
17
|
+
void metadata;
|
|
18
|
+
throw new Error('Auth module not configured. Inject a real auth module before issuing tokens.');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
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,8 @@
|
|
|
1
1
|
export { default as ApiServer } from './api-server-base.js';
|
|
2
|
-
export {
|
|
2
|
+
export { ApiError } from './api-server-base.js';
|
|
3
|
+
export { ApiModule } from './api-module.js';
|
|
3
4
|
export type { ApiErrorParams, ApiHandler, ApiKey, ApiServerConf, ApiRequest, ApiRoute, ApiAuthType, ApiAuthClass, ApiTokenData, RequestWithStuff } from './api-server-base.js';
|
|
5
|
+
export type { AuthIdentifier, AuthTokenMetadata, AuthTokenData, AuthTokenQuery, AuthTokenPair, AuthTokenPayload, PasskeyChallengeParams, PasskeyChallenge, PasskeyVerificationParams, PasskeyVerificationResult, OAuthClient, AuthCodeData, AuthCodeRequest, AuthStorage } from './auth-storage.js';
|
|
6
|
+
export type { AuthProviderModule } from './auth-module.js';
|
|
7
|
+
export { nullAuthStorage, BaseAuthStorage } from './auth-storage.js';
|
|
8
|
+
export { nullAuthModule, BaseAuthModule } from './auth-module.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { default as ApiServer } from './api-server-base.js';
|
|
2
|
-
export {
|
|
2
|
+
export { ApiError } from './api-server-base.js';
|
|
3
|
+
export { ApiModule } from './api-module.js';
|
|
4
|
+
export { nullAuthStorage, BaseAuthStorage } from './auth-storage.js';
|
|
5
|
+
export { nullAuthModule, BaseAuthModule } from './auth-module.js';
|