structured-fw 1.7.1 → 1.7.3

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.md CHANGED
@@ -142,6 +142,13 @@ new Application(config);
142
142
  - `beforeAssetAccess` - runs when assets are being accessed, before response is sent. Callback receives `RequestContext` as the first argument
143
143
  - `afterAssetAccess` - runs when assets are being accessed, after response is sent. Callback receives `RequestContext` as the first argument
144
144
  - `pageNotFound` - runs when a request is received for which there is no registered request handler (route), and the requested URL is not an asset. Callback's result is sent as a response - a good use case is showing a 404 page. Callback receives `RequestContext` as the first argument
145
+ - `sessionsStart` - runs after session handling is enabled, payload is instance of `Session`
146
+ - `sessionsStop` - runs after session handling is disabled, payload is instance of `Session`
147
+ - `sessionCreated` - runs after a new session is created, payload is `SessionEntry`
148
+ - `sessionExpired` - runs after a session expires, payload is `sessionId`
149
+ - `sessionValueSet` - runs after a value is set in session, payload is `[sessionId, key, value]`
150
+ - `sessionValueRemove` - runs after a value is removed from session, payload is `[sessionId, key]`
151
+ - `sessionClear` - runs after session data is cleared, payload is `string` (sessionId)
145
152
  - **Callback to any of the `ApplicationEvents` is expected to be an async function**
146
153
  - `importEnv<T extends LooseObject>(smartPrimitives: boolean = true): T` - import ENV variables that start with `StructuredConfig.envPrefix`_ (if envPrefix is omitted from config, all ENV variables are returned). It is a generic method so that you can specify the expected return type. If `smartPrimitives = true` importEnv will convert the ENV values to type it feels is appropriate:
147
154
  - numeric values -> `number`
@@ -273,6 +280,11 @@ class RequestContext<Body extends LooseObject | undefined = LooseObject> = {
273
280
  request: IncomingMessage;
274
281
  response: ServerResponse;
275
282
 
283
+ // client IP address as determined by common header names
284
+ // falls back to request.socket.remoteAddress
285
+ // null if all attempts to determine the IP address have failed
286
+ ipAddress: string | null;
287
+
276
288
  // captured URI arguments
277
289
  // for example if the requested URI was /users/8 and the pattern was /users/(userId:num)
278
290
  // args will be { userId: 8 }
@@ -9,6 +9,7 @@ import { Request } from './Request.js';
9
9
  import { Handlebars } from './Handlebars.js';
10
10
  import { Cookies } from './Cookies.js';
11
11
  import { RequestContext } from './RequestContext.js';
12
+ import { SessionEntry } from '../Types.js';
12
13
  export declare class Application {
13
14
  readonly config: StructuredConfig;
14
15
  initialized: boolean;
@@ -25,7 +26,7 @@ export declare class Application {
25
26
  constructor(config: StructuredConfig);
26
27
  init(): Promise<void>;
27
28
  private start;
28
- on<E extends ApplicationEvents>(evt: E, callback: (payload: E extends 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' | 'requestHandleError' ? RequestContext<RequestContextData> : E extends 'documentCreated' ? Document : E extends 'afterComponentsLoaded' ? Components : E extends 'serverStarted' ? Server : undefined) => void): void;
29
+ on<E extends ApplicationEvents>(evt: E, callback: (payload: E extends 'beforeRequestHandler' | 'afterRequestHandler' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' | 'requestHandleError' ? RequestContext<RequestContextData> : E extends 'documentCreated' ? Document : E extends 'afterComponentsLoaded' ? Components : E extends 'serverStarted' ? Server : E extends 'sessionsStart' ? Session : E extends 'sessionsStop' ? Session : E extends 'sessionCreated' ? SessionEntry : E extends 'sessionExpired' ? string : E extends 'sessionValueSet' ? [string, string, any] : E extends 'sessionValueRemove' ? [string, string] : E extends 'sessionClear' ? string : undefined) => void): void;
29
30
  emit(eventName: ApplicationEvents, payload?: any): Promise<Array<any>>;
30
31
  importEnv<T extends LooseObject>(smartPrimitives?: boolean): T;
31
32
  exportContextFields(...fields: Array<keyof RequestContextData>): void;
@@ -29,7 +29,6 @@ export class Application {
29
29
  this.session = new Session(this);
30
30
  this.request = new Request(this);
31
31
  this.components = new Components(this);
32
- this.session.start();
33
32
  if (this.config.autoInit) {
34
33
  this.init();
35
34
  }
@@ -73,6 +72,7 @@ export class Application {
73
72
  return '';
74
73
  }, this, true);
75
74
  await this.start();
75
+ this.session.start();
76
76
  this.initialized = true;
77
77
  }
78
78
  start() {
@@ -11,6 +11,7 @@ export declare class RequestContext<Body extends LooseObject | undefined = Loose
11
11
  private readonly handler;
12
12
  readonly request: IncomingMessage;
13
13
  readonly response: ServerResponse;
14
+ ipAddress: string | null;
14
15
  args: URIArguments;
15
16
  cookies: Record<string, string>;
16
17
  body: Body;
@@ -41,4 +42,5 @@ export declare class RequestContext<Body extends LooseObject | undefined = Loose
41
42
  duration(): number;
42
43
  complete(): boolean;
43
44
  isAsset(): boolean;
45
+ private requestIPAddress;
44
46
  }
@@ -14,6 +14,7 @@ export class RequestContext {
14
14
  handler;
15
15
  request;
16
16
  response;
17
+ ipAddress = null;
17
18
  args = {};
18
19
  cookies = {};
19
20
  body;
@@ -32,6 +33,7 @@ export class RequestContext {
32
33
  this.response = response;
33
34
  this.handler = handler;
34
35
  this.body = undefined;
36
+ this.ipAddress = this.requestIPAddress();
35
37
  }
36
38
  async exec() {
37
39
  if (this.executionStartedAt !== null) {
@@ -332,4 +334,32 @@ export class RequestContext {
332
334
  });
333
335
  });
334
336
  }
337
+ requestIPAddress() {
338
+ const headerList = [
339
+ 'x-client-ip',
340
+ 'x-real-ip',
341
+ 'cf-connecting-ip',
342
+ 'true-client-ip',
343
+ 'x-cluster-client-ip',
344
+ 'x-forwarded-for',
345
+ ];
346
+ for (const headerName of headerList) {
347
+ if (headerName in this.request.headers) {
348
+ const value = this.request.headers[headerName];
349
+ if (typeof value === 'string' && value.trim().length > 0) {
350
+ if (headerName === 'x-forwarded-for') {
351
+ const ips = value.split(',');
352
+ const ip = ips[0].trim();
353
+ if (ip.length > 0) {
354
+ return ip;
355
+ }
356
+ }
357
+ else {
358
+ return value.trim();
359
+ }
360
+ }
361
+ }
362
+ }
363
+ return this.request.socket.remoteAddress || null;
364
+ }
335
365
  }
@@ -10,6 +10,7 @@ export declare class Session {
10
10
  constructor(app: Application);
11
11
  start(): void;
12
12
  stop(): void;
13
+ load(sessions: Record<string, SessionEntry>): void;
13
14
  private sessionInit;
14
15
  private generateId;
15
16
  private garbageCollect;
@@ -25,9 +25,14 @@ export class Session {
25
25
  }
26
26
  start() {
27
27
  this.enabled = true;
28
+ this.application.emit('sessionsStart', this);
28
29
  }
29
30
  stop() {
30
31
  this.enabled = false;
32
+ this.application.emit('sessionsStop', this);
33
+ }
34
+ load(sessions) {
35
+ this.sessions = sessions;
31
36
  }
32
37
  sessionInit(ctx) {
33
38
  ctx.sessionId = this.generateId();
@@ -38,6 +43,7 @@ export class Session {
38
43
  data: {}
39
44
  };
40
45
  this.sessions[ctx.sessionId] = sessionEntry;
46
+ this.application.emit('sessionCreated', sessionEntry);
41
47
  }
42
48
  generateId() {
43
49
  return randomString(this.application.config.session.keyLength);
@@ -49,6 +55,7 @@ export class Session {
49
55
  const sess = this.sessions[sessionId];
50
56
  if (time - sess.lastRequest > sessDurationMilliseconds) {
51
57
  delete this.sessions[sessionId];
58
+ this.application.emit('sessionExpired', sessionId);
52
59
  }
53
60
  }
54
61
  setTimeout(() => {
@@ -62,6 +69,7 @@ export class Session {
62
69
  if (this.sessions[sessionId]) {
63
70
  const session = this.sessions[sessionId];
64
71
  session.data[key] = value;
72
+ this.application.emit('sessionValueSet', [sessionId, key, value]);
65
73
  }
66
74
  }
67
75
  getValue(sessionId, key) {
@@ -85,6 +93,7 @@ export class Session {
85
93
  }
86
94
  if (this.sessions[sessionId] && this.sessions[sessionId].data[key]) {
87
95
  delete this.sessions[sessionId].data[key];
96
+ this.application.emit('sessionValueRemove', [sessionId, key]);
88
97
  }
89
98
  }
90
99
  clear(sessionId) {
@@ -93,6 +102,7 @@ export class Session {
93
102
  }
94
103
  if (this.sessions[sessionId]) {
95
104
  this.sessions[sessionId].data = {};
105
+ this.application.emit('sessionClear', sessionId);
96
106
  }
97
107
  }
98
108
  extract(sessionId, keys) {
@@ -1 +1 @@
1
- export type ApplicationEvents = 'serverStarted' | 'beforeRequestHandler' | 'afterRequestHandler' | 'requestHandleError' | 'beforeRoutes' | 'afterRoutes' | 'beforeComponentsLoad' | 'afterComponentsLoaded' | 'documentCreated' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound';
1
+ export type ApplicationEvents = 'serverStarted' | 'beforeRequestHandler' | 'afterRequestHandler' | 'requestHandleError' | 'beforeRoutes' | 'afterRoutes' | 'beforeComponentsLoad' | 'afterComponentsLoaded' | 'documentCreated' | 'beforeAssetAccess' | 'afterAssetAccess' | 'pageNotFound' | 'sessionsStart' | 'sessionsStop' | 'sessionCreated' | 'sessionExpired' | 'sessionValueSet' | 'sessionValueRemove' | 'sessionClear';
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "license": "MIT",
20
20
  "type": "module",
21
21
  "main": "build/index",
22
- "version": "1.7.1",
22
+ "version": "1.7.3",
23
23
  "scripts": {
24
24
  "develop": "tsc --watch",
25
25
  "startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,hbs,css index.js",