@technomoron/api-server-base 1.1.6 → 1.1.8

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
@@ -109,6 +109,7 @@ accessCookie (string, default 'dat') Access token cookie name.
109
109
  refreshCookie (string, default 'drt') Refresh token cookie name.
110
110
  accessExpiry (number, default 60 * 15) Access token lifetime in seconds.
111
111
  refreshExpiry (number, default 30 * 24 * 60 * 60 * 1000) Refresh token lifetime in milliseconds.
112
+ sessionRefreshExpiry (number, default 24 * 60 * 60) Session token lifetime in seconds when clients opt out of "remember me" cookies.
112
113
  authApi (boolean, default false) Toggle you can use when mounting auth routes.
113
114
  devMode (boolean, default false) Custom hook for development only features.
114
115
  debug (boolean, default false) When true the server logs inbound requests via dumpRequest.
@@ -326,6 +326,7 @@ function fillConfig(config) {
326
326
  refreshCookie: config.refreshCookie ?? 'drt',
327
327
  accessExpiry: config.accessExpiry ?? 60 * 15,
328
328
  refreshExpiry: config.refreshExpiry ?? 30 * 24 * 60 * 60,
329
+ sessionRefreshExpiry: config.sessionRefreshExpiry ?? 24 * 60 * 60,
329
330
  authApi: config.authApi ?? false,
330
331
  devMode: config.devMode ?? false,
331
332
  hydrateGetBody: config.hydrateGetBody ?? true,
@@ -335,7 +336,9 @@ function fillConfig(config) {
335
336
  class ApiServer {
336
337
  constructor(config = {}) {
337
338
  this.currReq = null;
339
+ this.apiNotFoundHandler = null;
338
340
  this.config = fillConfig(config);
341
+ this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
339
342
  this.storageAdapter = auth_storage_js_1.nullAuthStorage;
340
343
  this.moduleAdapter = auth_module_js_1.nullAuthModule;
341
344
  this.app = (0, express_1.default)();
@@ -345,6 +348,7 @@ class ApiServer {
345
348
  }
346
349
  this.middlewares();
347
350
  // addSwaggerUi(this.app);
351
+ this.installApiNotFoundHandler();
348
352
  }
349
353
  authStorage(storage) {
350
354
  this.storageAdapter = storage;
@@ -493,6 +497,56 @@ class ApiServer {
493
497
  };
494
498
  this.app.use((0, cors_1.default)(corsOptions));
495
499
  }
500
+ normalizeApiBasePath(path) {
501
+ if (!path || typeof path !== 'string') {
502
+ return '/api';
503
+ }
504
+ const trimmed = path.trim();
505
+ if (!trimmed) {
506
+ return '/api';
507
+ }
508
+ const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
509
+ if (withLeadingSlash.length === 1) {
510
+ return withLeadingSlash;
511
+ }
512
+ return withLeadingSlash.replace(/\/+$/, '') || '/api';
513
+ }
514
+ installApiNotFoundHandler() {
515
+ if (this.apiNotFoundHandler) {
516
+ return;
517
+ }
518
+ this.apiNotFoundHandler = (req, res) => {
519
+ const payload = {
520
+ code: 404,
521
+ message: this.describeMissingEndpoint(req),
522
+ data: null,
523
+ errors: {}
524
+ };
525
+ res.status(404).json(payload);
526
+ };
527
+ this.app.use(this.apiBasePath, this.apiNotFoundHandler);
528
+ }
529
+ ensureApiNotFoundOrdering() {
530
+ this.installApiNotFoundHandler();
531
+ if (!this.apiNotFoundHandler) {
532
+ return;
533
+ }
534
+ const stack = this.app?._router?.stack;
535
+ if (!stack) {
536
+ return;
537
+ }
538
+ const index = stack.findIndex((layer) => layer.handle === this.apiNotFoundHandler);
539
+ if (index === -1 || index === stack.length - 1) {
540
+ return;
541
+ }
542
+ const [layer] = stack.splice(index, 1);
543
+ stack.push(layer);
544
+ }
545
+ describeMissingEndpoint(req) {
546
+ const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
547
+ const target = req.originalUrl || req.url || this.apiBasePath;
548
+ return `No such endpoint: ${method} ${target}`;
549
+ }
496
550
  start() {
497
551
  this.app
498
552
  .listen({
@@ -705,7 +759,7 @@ class ApiServer {
705
759
  this.authModule(module);
706
760
  }
707
761
  module.checkConfig();
708
- const base = this.config.apiBasePath ?? '/api';
762
+ const base = this.apiBasePath;
709
763
  const ns = module.namespace;
710
764
  const mountPath = `${base}${ns}`;
711
765
  module.mountpath = mountPath;
@@ -717,6 +771,7 @@ class ApiServer {
717
771
  }
718
772
  });
719
773
  this.app.use(mountPath, router);
774
+ this.ensureApiNotFoundOrdering();
720
775
  return this;
721
776
  }
722
777
  dumpRequest(apiReq) {
@@ -92,6 +92,7 @@ export interface ApiServerConf {
92
92
  refreshCookie: string;
93
93
  accessExpiry: number;
94
94
  refreshExpiry: number;
95
+ sessionRefreshExpiry: number;
95
96
  authApi: boolean;
96
97
  devMode: boolean;
97
98
  hydrateGetBody: boolean;
@@ -101,8 +102,10 @@ export declare class ApiServer {
101
102
  app: Application;
102
103
  currReq: ApiRequest | null;
103
104
  readonly config: ApiServerConf;
105
+ private readonly apiBasePath;
104
106
  private storageAdapter;
105
107
  private moduleAdapter;
108
+ private apiNotFoundHandler;
106
109
  constructor(config?: Partial<ApiServerConf>);
107
110
  authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
108
111
  /**
@@ -134,6 +137,10 @@ export declare class ApiServer {
134
137
  guessExceptionText(error: any, defMsg?: string): string;
135
138
  protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
136
139
  private middlewares;
140
+ private normalizeApiBasePath;
141
+ private installApiNotFoundHandler;
142
+ private ensureApiNotFoundOrdering;
143
+ private describeMissingEndpoint;
137
144
  start(): this;
138
145
  private verifyJWT;
139
146
  private authenticate;
@@ -10,6 +10,8 @@ export interface AuthTokenMetadata {
10
10
  os?: string;
11
11
  scope?: string | string[];
12
12
  loginType?: string;
13
+ refreshTtlSeconds?: number;
14
+ sessionCookie?: boolean;
13
15
  revokeSessions?: 'device' | 'domain' | 'client' | 'user';
14
16
  }
15
17
  export interface AuthTokenData extends AuthTokenMetadata {
@@ -92,6 +92,7 @@ export interface ApiServerConf {
92
92
  refreshCookie: string;
93
93
  accessExpiry: number;
94
94
  refreshExpiry: number;
95
+ sessionRefreshExpiry: number;
95
96
  authApi: boolean;
96
97
  devMode: boolean;
97
98
  hydrateGetBody: boolean;
@@ -101,8 +102,10 @@ export declare class ApiServer {
101
102
  app: Application;
102
103
  currReq: ApiRequest | null;
103
104
  readonly config: ApiServerConf;
105
+ private readonly apiBasePath;
104
106
  private storageAdapter;
105
107
  private moduleAdapter;
108
+ private apiNotFoundHandler;
106
109
  constructor(config?: Partial<ApiServerConf>);
107
110
  authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
108
111
  /**
@@ -134,6 +137,10 @@ export declare class ApiServer {
134
137
  guessExceptionText(error: any, defMsg?: string): string;
135
138
  protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
136
139
  private middlewares;
140
+ private normalizeApiBasePath;
141
+ private installApiNotFoundHandler;
142
+ private ensureApiNotFoundOrdering;
143
+ private describeMissingEndpoint;
137
144
  start(): this;
138
145
  private verifyJWT;
139
146
  private authenticate;
@@ -318,6 +318,7 @@ function fillConfig(config) {
318
318
  refreshCookie: config.refreshCookie ?? 'drt',
319
319
  accessExpiry: config.accessExpiry ?? 60 * 15,
320
320
  refreshExpiry: config.refreshExpiry ?? 30 * 24 * 60 * 60,
321
+ sessionRefreshExpiry: config.sessionRefreshExpiry ?? 24 * 60 * 60,
321
322
  authApi: config.authApi ?? false,
322
323
  devMode: config.devMode ?? false,
323
324
  hydrateGetBody: config.hydrateGetBody ?? true,
@@ -327,7 +328,9 @@ function fillConfig(config) {
327
328
  export class ApiServer {
328
329
  constructor(config = {}) {
329
330
  this.currReq = null;
331
+ this.apiNotFoundHandler = null;
330
332
  this.config = fillConfig(config);
333
+ this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
331
334
  this.storageAdapter = nullAuthStorage;
332
335
  this.moduleAdapter = nullAuthModule;
333
336
  this.app = express();
@@ -337,6 +340,7 @@ export class ApiServer {
337
340
  }
338
341
  this.middlewares();
339
342
  // addSwaggerUi(this.app);
343
+ this.installApiNotFoundHandler();
340
344
  }
341
345
  authStorage(storage) {
342
346
  this.storageAdapter = storage;
@@ -485,6 +489,56 @@ export class ApiServer {
485
489
  };
486
490
  this.app.use(cors(corsOptions));
487
491
  }
492
+ normalizeApiBasePath(path) {
493
+ if (!path || typeof path !== 'string') {
494
+ return '/api';
495
+ }
496
+ const trimmed = path.trim();
497
+ if (!trimmed) {
498
+ return '/api';
499
+ }
500
+ const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
501
+ if (withLeadingSlash.length === 1) {
502
+ return withLeadingSlash;
503
+ }
504
+ return withLeadingSlash.replace(/\/+$/, '') || '/api';
505
+ }
506
+ installApiNotFoundHandler() {
507
+ if (this.apiNotFoundHandler) {
508
+ return;
509
+ }
510
+ this.apiNotFoundHandler = (req, res) => {
511
+ const payload = {
512
+ code: 404,
513
+ message: this.describeMissingEndpoint(req),
514
+ data: null,
515
+ errors: {}
516
+ };
517
+ res.status(404).json(payload);
518
+ };
519
+ this.app.use(this.apiBasePath, this.apiNotFoundHandler);
520
+ }
521
+ ensureApiNotFoundOrdering() {
522
+ this.installApiNotFoundHandler();
523
+ if (!this.apiNotFoundHandler) {
524
+ return;
525
+ }
526
+ const stack = this.app?._router?.stack;
527
+ if (!stack) {
528
+ return;
529
+ }
530
+ const index = stack.findIndex((layer) => layer.handle === this.apiNotFoundHandler);
531
+ if (index === -1 || index === stack.length - 1) {
532
+ return;
533
+ }
534
+ const [layer] = stack.splice(index, 1);
535
+ stack.push(layer);
536
+ }
537
+ describeMissingEndpoint(req) {
538
+ const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
539
+ const target = req.originalUrl || req.url || this.apiBasePath;
540
+ return `No such endpoint: ${method} ${target}`;
541
+ }
488
542
  start() {
489
543
  this.app
490
544
  .listen({
@@ -697,7 +751,7 @@ export class ApiServer {
697
751
  this.authModule(module);
698
752
  }
699
753
  module.checkConfig();
700
- const base = this.config.apiBasePath ?? '/api';
754
+ const base = this.apiBasePath;
701
755
  const ns = module.namespace;
702
756
  const mountPath = `${base}${ns}`;
703
757
  module.mountpath = mountPath;
@@ -709,6 +763,7 @@ export class ApiServer {
709
763
  }
710
764
  });
711
765
  this.app.use(mountPath, router);
766
+ this.ensureApiNotFoundOrdering();
712
767
  return this;
713
768
  }
714
769
  dumpRequest(apiReq) {
@@ -10,6 +10,8 @@ export interface AuthTokenMetadata {
10
10
  os?: string;
11
11
  scope?: string | string[];
12
12
  loginType?: string;
13
+ refreshTtlSeconds?: number;
14
+ sessionCookie?: boolean;
13
15
  revokeSessions?: 'device' | 'domain' | 'client' | 'user';
14
16
  }
15
17
  export interface AuthTokenData extends AuthTokenMetadata {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/api-server-base",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Api Server Skeleton / Base Class",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",
@@ -28,14 +28,14 @@
28
28
  "scrub": "rm -rf ./node_modules/ pnpm-lock.yaml ./dist/",
29
29
  "build:cjs": "tsc --project tsconfig/tsconfig.cjs.json && node scripts/prepare-cjs.cjs",
30
30
  "build:esm": "tsc --project tsconfig/tsconfig.esm.json",
31
- "build": "npm run build:cjs && npm run build:esm",
31
+ "build": "node scripts/run-builds.cjs",
32
32
  "test": "vitest run",
33
33
  "test:watch": "vitest --watch",
34
- "prepublishOnly": "npm run build:cjs && npm run build:esm",
34
+ "prepublishOnly": "node scripts/run-builds.cjs",
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}\" && npm run build",
38
+ "cleanbuild": "rm -rf ./dist/ && eslint --fix --ext .js,.ts,.vue ./ && prettier --log-level warn --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\" && node scripts/run-builds.cjs",
39
39
  "pretty": "prettier --write \"**/*.{js,jsx,ts,tsx,vue,json,css,scss,md}\""
40
40
  },
41
41
  "dependencies": {
@@ -65,6 +65,11 @@
65
65
  "typescript": "^5.9.3",
66
66
  "vitest": "^3.2.4"
67
67
  },
68
+ "pnpm": {
69
+ "onlyBuiltDependencies": [
70
+ "esbuild"
71
+ ]
72
+ },
68
73
  "files": [
69
74
  "dist/",
70
75
  "package.json"