@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 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
 
@@ -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
- 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);
181
288
  }
182
289
  async authenticateUser(params) {
183
- 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);
184
299
  }
185
- async storeToken(params) {
186
- throw new Error('storeToken() not implemented');
300
+ async storeToken(data) {
301
+ await this.storageAdapter.storeToken(data);
187
302
  }
188
- async getToken(params) {
189
- throw new Error('getToken() not implemented');
303
+ async getToken(query) {
304
+ return this.storageAdapter.getToken(query);
190
305
  }
191
- async updateToken(params) {
192
- 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
+ });
193
317
  }
194
- async deleteToken(params) {
195
- throw new Error('deleteToken() not implemented');
318
+ async deleteToken(query) {
319
+ return this.storageAdapter.deleteToken(query);
196
320
  }
197
321
  async verifyPassword(password, hash) {
198
- throw new Error('verifyPassword() not implemented');
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: 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;
@@ -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>;
@@ -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;
@@ -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
- 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);
173
280
  }
174
281
  async authenticateUser(params) {
175
- 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);
176
291
  }
177
- async storeToken(params) {
178
- throw new Error('storeToken() not implemented');
292
+ async storeToken(data) {
293
+ await this.storageAdapter.storeToken(data);
179
294
  }
180
- async getToken(params) {
181
- throw new Error('getToken() not implemented');
295
+ async getToken(query) {
296
+ return this.storageAdapter.getToken(query);
182
297
  }
183
- async updateToken(params) {
184
- 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
+ });
185
309
  }
186
- async deleteToken(params) {
187
- throw new Error('deleteToken() not implemented');
310
+ async deleteToken(query) {
311
+ return this.storageAdapter.deleteToken(query);
188
312
  }
189
313
  async verifyPassword(password, hash) {
190
- throw new Error('verifyPassword() not implemented');
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();
@@ -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.42",
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}\" && tsc --project tsconfig/tsconfig.cjs.json && tsc --project tsconfig/tsconfig.esm.json",
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.21",
45
- "@types/jsonwebtoken": "^9.0.9",
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.1"
51
+ "multer": "^2.0.2"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/express-serve-static-core": "^5.0.6",
54
+ "@types/express-serve-static-core": "^5.1.0",
55
55
  "@types/supertest": "^6.0.3",
56
- "@typescript-eslint/eslint-plugin": "^8.33.0",
57
- "@typescript-eslint/parser": "^8.33.0",
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.28.0",
61
- "eslint-plugin-import": "^2.31.0",
62
- "eslint-plugin-vue": "^10.1.0",
63
- "prettier": "^3.5.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.8.3",
65
+ "typescript": "^5.9.3",
66
66
  "vitest": "^3.2.4"
67
67
  },
68
68
  "files": [