@technomoron/api-server-base 1.0.43 → 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 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
- server.api(new UserModule()).start();
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
 
@@ -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 = '';
@@ -168,6 +170,8 @@ class ApiServer {
168
170
  constructor(config = {}) {
169
171
  this.currReq = null;
170
172
  this.config = fillConfig(config);
173
+ this.storageAdapter = auth_storage_js_1.nullAuthStorage;
174
+ this.moduleAdapter = auth_module_js_1.nullAuthModule;
171
175
  this.app = (0, express_1.default)();
172
176
  if (config.uploadPath) {
173
177
  const upload = (0, multer_1.default)({ dest: config.uploadPath });
@@ -176,6 +180,32 @@ class ApiServer {
176
180
  this.middlewares();
177
181
  // addSwaggerUi(this.app);
178
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
+ }
179
209
  jwtSign(payload, secret, expiresInSeconds, options) {
180
210
  options || (options = {});
181
211
  const opts = { ...options, expiresIn: expiresInSeconds };
@@ -248,36 +278,51 @@ class ApiServer {
248
278
  return null;
249
279
  }
250
280
  async getUser(uid) {
251
- void uid;
252
- throw new Error('getUser() not implemented');
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);
253
288
  }
254
289
  async authenticateUser(params) {
255
- void params;
256
- throw new Error('authenticateUser() not implemented');
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);
257
299
  }
258
- async storeToken(params) {
259
- void params;
260
- throw new Error('storeToken() not implemented');
300
+ async storeToken(data) {
301
+ await this.storageAdapter.storeToken(data);
261
302
  }
262
- async getToken(params) {
263
- void params;
264
- throw new Error('getToken() not implemented');
303
+ async getToken(query) {
304
+ return this.storageAdapter.getToken(query);
265
305
  }
266
- async updateToken(params) {
267
- void params;
268
- throw new Error('updateToken() not implemented');
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
+ });
269
317
  }
270
- async deleteToken(params) {
271
- void params;
272
- throw new Error('deleteToken() not implemented');
318
+ async deleteToken(query) {
319
+ return this.storageAdapter.deleteToken(query);
273
320
  }
274
321
  async verifyPassword(password, hash) {
275
- void password;
276
- void hash;
277
- throw new Error('verifyPassword() not implemented');
322
+ return this.storageAdapter.verifyPassword(password, hash);
278
323
  }
279
324
  filterUser(fullUser) {
280
- return fullUser;
325
+ return this.storageAdapter.filterUser(fullUser);
281
326
  }
282
327
  guessExceptionText(error, defMsg = 'Unkown Error') {
283
328
  return guess_exception_text(error, defMsg);
@@ -495,6 +540,9 @@ class ApiServer {
495
540
  api(module) {
496
541
  const router = express_1.default.Router();
497
542
  module.server = this;
543
+ if (module?.moduleType === 'auth') {
544
+ this.authModule(module);
545
+ }
498
546
  module.checkConfig();
499
547
  const base = this.config.apiBasePath ?? '/api';
500
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: unknown): Promise<unknown>;
113
- authenticateUser(params: unknown): Promise<boolean>;
114
- storeToken(params: {
115
- access: string;
116
- refresh: string;
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: {
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(params: {
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;
@@ -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>;
@@ -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; } });
@@ -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: unknown): Promise<unknown>;
113
- authenticateUser(params: unknown): Promise<boolean>;
114
- storeToken(params: {
115
- access: string;
116
- refresh: string;
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: {
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(params: {
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;
@@ -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 = '';
@@ -160,6 +162,8 @@ export class ApiServer {
160
162
  constructor(config = {}) {
161
163
  this.currReq = null;
162
164
  this.config = fillConfig(config);
165
+ this.storageAdapter = nullAuthStorage;
166
+ this.moduleAdapter = nullAuthModule;
163
167
  this.app = express();
164
168
  if (config.uploadPath) {
165
169
  const upload = multer({ dest: config.uploadPath });
@@ -168,6 +172,32 @@ export class ApiServer {
168
172
  this.middlewares();
169
173
  // addSwaggerUi(this.app);
170
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
+ }
171
201
  jwtSign(payload, secret, expiresInSeconds, options) {
172
202
  options || (options = {});
173
203
  const opts = { ...options, expiresIn: expiresInSeconds };
@@ -240,36 +270,51 @@ export class ApiServer {
240
270
  return null;
241
271
  }
242
272
  async getUser(uid) {
243
- void uid;
244
- throw new Error('getUser() not implemented');
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);
245
280
  }
246
281
  async authenticateUser(params) {
247
- void params;
248
- throw new Error('authenticateUser() not implemented');
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);
249
291
  }
250
- async storeToken(params) {
251
- void params;
252
- throw new Error('storeToken() not implemented');
292
+ async storeToken(data) {
293
+ await this.storageAdapter.storeToken(data);
253
294
  }
254
- async getToken(params) {
255
- void params;
256
- throw new Error('getToken() not implemented');
295
+ async getToken(query) {
296
+ return this.storageAdapter.getToken(query);
257
297
  }
258
- async updateToken(params) {
259
- void params;
260
- throw new Error('updateToken() not implemented');
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
+ });
261
309
  }
262
- async deleteToken(params) {
263
- void params;
264
- throw new Error('deleteToken() not implemented');
310
+ async deleteToken(query) {
311
+ return this.storageAdapter.deleteToken(query);
265
312
  }
266
313
  async verifyPassword(password, hash) {
267
- void password;
268
- void hash;
269
- throw new Error('verifyPassword() not implemented');
314
+ return this.storageAdapter.verifyPassword(password, hash);
270
315
  }
271
316
  filterUser(fullUser) {
272
- return fullUser;
317
+ return this.storageAdapter.filterUser(fullUser);
273
318
  }
274
319
  guessExceptionText(error, defMsg = 'Unkown Error') {
275
320
  return guess_exception_text(error, defMsg);
@@ -487,6 +532,9 @@ export class ApiServer {
487
532
  api(module) {
488
533
  const router = express.Router();
489
534
  module.server = this;
535
+ if (module?.moduleType === 'auth') {
536
+ this.authModule(module);
537
+ }
490
538
  module.checkConfig();
491
539
  const base = this.config.apiBasePath ?? '/api';
492
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();
@@ -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
@@ -1,2 +1,4 @@
1
1
  export { default as ApiServer } from './api-server-base.js';
2
2
  export { ApiModule, ApiError } from './api-server-base.js';
3
+ export { nullAuthStorage, BaseAuthStorage } from './auth-storage.js';
4
+ export { nullAuthModule, BaseAuthModule } from './auth-module.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/api-server-base",
3
- "version": "1.0.43",
3
+ "version": "1.1.0",
4
4
  "description": "Api Server Skeleton / Base Class",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",