scimgateway 5.2.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,15 +15,14 @@ Validated through IdP's:
15
15
  - SailPoint/IdentityNow
16
16
 
17
17
  Latest news:
18
-
19
- - Centralized logging and monitoring through online log subscription
18
+
19
+ - Remote real-time log subscription for monitoring and centralized logging
20
20
  using browser and url: https://host/logger
21
21
  curl -N https://host/logger -u gwread:password
22
- curl -N https://host/logger -H "Authorization: Bearer secret"
23
22
  custom client API, see configuration notes
24
23
  - By configuring the chainingBaseUrl, it is now possible to chain multiple gateways in sequence, such as `gateway1->gateway2->gateway3->endpoint`. In this setup, gateway beave much like a reverse proxy, validating authorization at each step unless PassThrough mode is enabled. Chaining is also supported in stream subscriber mode
25
24
  - Email, onError and sendMail() supports more secure RESTful OAuth for Microsoft Exchange Online (ExO) and Google Workspace Gmail, alongside traditional SMTP Auth for all mail systems. HelperRest supports a wide range of common authentication methods, including basicAuth, bearerAuth, tokenAuth, oauth, oauthSamlBearer, oauthJwtBearer and Auth PassTrough
26
- - Major version **v5.0.0** marks a shift to native TypeScript support and prioritizes [Bun](https://bun.sh/) over Node.js. This upgrade requires some modifications to existing plugins.
25
+ - Major version **v5.0.0** marks a shift from JavaScript to native TypeScript and prioritizes [Bun](https://bun.sh/) over Node.js. This upgrade requires some modifications to existing plugins.
27
26
  - **BREAKING**: [SCIM Stream](https://elshaug.xyz/docs/scim-stream) is the modern way of user provisioning letting clients subscribe to messages instead of traditional IGA top-down provisioning. SCIM Gateway now offers enhanced functionality with support for message subscription and automated provisioning using SCIM Stream
28
27
  - Authentication PassThrough letting plugin pass authentication directly to endpoint for avoid maintaining secrets at the gateway. E.g., using Entra ID application OAuth
29
28
  - Supports OAuth Client Credentials authentication
@@ -735,8 +734,8 @@ Example using general OAuth:
735
734
 
736
735
  Please see code editor method HelperRest doRequest() IntelliSense for type and option details
737
736
 
738
- ### Configuration notes - Centralized logging and monitoring
739
- We may subscribe for online log events using `GET /logger` e.g.:
737
+ ### Configuration notes - Remote real-time log subscription
738
+ We may have monitoring and centralized logging through remote real-time log subscription
740
739
 
741
740
  - using browser and url: https://host/logger
742
741
  - curl -N https://host/logger -u gwread:password
@@ -804,11 +803,11 @@ Example code using custom subscriber API for log collection and monitoring
804
803
  console.log('Now awaiting log events..\n')
805
804
 
806
805
  while (true) {
807
- const { value, done } = await reader.read();
806
+ const { value, done } = await reader.read()
808
807
  if (done) break;
809
808
  if (value.at(-1) !== '\n') continue
810
809
  const message = value.slice(0, -1)
811
- await messageHandler(message)
810
+ messageHandler(message)
812
811
  }
813
812
 
814
813
  // shouldn't be here... authentication failure?
@@ -1402,6 +1401,12 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1402
1401
 
1403
1402
  ## Change log
1404
1403
 
1404
+ ### v5.2.1
1405
+
1406
+ [Fixed]
1407
+
1408
+ - Logger did not use the correct plugin rollover filename when the gateway ran multiple plugins
1409
+
1405
1410
  ### v5.2.0
1406
1411
 
1407
1412
  [Improved]
@@ -1409,12 +1414,12 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1409
1414
  - Logger have been redesigned
1410
1415
 
1411
1416
  Supports console, file and push (client subscriber) logging
1412
- Centralized logging and monitoring through online log subscription, see configuration notes
1417
+ Remote real-time log subscription, see configuration notes
1413
1418
  JSON formatted log messages
1414
1419
  UTC (Coordinated Universal Time)
1415
1420
  File logging will rotate on startup
1416
1421
  File logging now includes configuration options for maxFiles and maxSize
1417
- Console using default colorized and minimized output, if redirected to stdout/stderr standard JSON will be used and no color encoding
1422
+ Console using default colorized and minimized output. If redirecting stdout/stderr, standard JSON will be used and no color encoding
1418
1423
 
1419
1424
 
1420
1425
  ### v5.1.8
package/lib/logger.ts CHANGED
@@ -8,15 +8,6 @@ import { existsSync, renameSync, readdirSync, unlinkSync, mkdirSync, createWrite
8
8
  import { join } from 'node:path'
9
9
  import diagnostics_channel from 'node:diagnostics_channel'
10
10
 
11
- let LOG_DIR = './logs'
12
- let LOG_FILE_PREFIX = 'app'
13
- let LOG_FILE_SUFFIX = 'log'
14
- let LOG_FILE_NAME = LOG_FILE_PREFIX + '.' + LOG_FILE_SUFFIX
15
- let LOG_FILE = LOG_DIR + '/' + LOG_FILE_NAME
16
- let MAX_LOG_SIZE = 20 * 1024 * 1024 // 20 MB max file size
17
- let MAX_LOG_FILES = 5 // keep only the last 5 logs - note, new and rotated file on startup
18
- const HIGH_WATER_MARK = 16 * 1024 // 16KB buffer size before auto-flushing
19
-
20
11
  // Node does not support "export enum LogLevel"
21
12
  // instead using LogLevel as object and the type "LogLevel"
22
13
  export const LogLevel = {
@@ -57,6 +48,29 @@ interface LoggerOptions {
57
48
  colorize?: boolean
58
49
  }
59
50
 
51
+ /**
52
+ * Example:
53
+ ```
54
+ const logger = new Logger(
55
+ 'plugin-loki',
56
+ {
57
+ type: 'console',
58
+ level: 'error',
59
+ customMasking: null,
60
+ colorize: true,
61
+ },
62
+ {
63
+ type: 'file',
64
+ level: 'debug',
65
+ customMasking: null,
66
+ logDir: '/opt/my-scimgateway/logs',
67
+ logFileName: 'plugin-loki.log',
68
+ maxSize: 20,
69
+ maxFiles: 5,
70
+ },
71
+ )
72
+ ```
73
+ */
60
74
  export class Logger {
61
75
  private logStream: any // either Bun's FileSink or Node's WriteStream
62
76
  private logChannel: diagnostics_channel.Channel
@@ -69,23 +83,40 @@ export class Logger {
69
83
  private reJson: RegExp
70
84
  private reXml: RegExp
71
85
  private callbacks: Set<(message: any) => Promise<void>> = new Set()
86
+ private LOG_DIR: string
87
+ private LOG_FILE_PREFIX: string
88
+ private LOG_FILE_SUFFIX: string
89
+ private LOG_FILE_NAME: string
90
+ private LOG_FILE: string
91
+ private MAX_LOG_SIZE: number
92
+ private MAX_LOG_FILES: number
93
+ private HIGH_WATER_MARK: number
72
94
 
73
95
  constructor(category: string, ...options: LoggerOptions[]) {
96
+ this.LOG_DIR = './logs'
97
+ this.LOG_FILE_PREFIX = 'app'
98
+ this.LOG_FILE_SUFFIX = 'log'
99
+ this.LOG_FILE_NAME = this.LOG_FILE_PREFIX + '.' + this.LOG_FILE_SUFFIX
100
+ this.LOG_FILE = this.LOG_DIR + '/' + this.LOG_FILE_NAME
101
+ this.MAX_LOG_SIZE = 20 * 1024 * 1024 // 20 MB max file size
102
+ this.MAX_LOG_FILES = 5 // keep only the last 5 logs - note, new and rotated file on startup
103
+ this.HIGH_WATER_MARK = 16 * 1024 // 16KB buffer size before auto-flushing
104
+
74
105
  if (!category) throw Error('Logger constructor missing mandatory category')
75
106
  this.category = category
76
107
  for (const option of options) {
77
108
  if (option.type === 'file') {
78
- if (option.logDir) LOG_DIR = option.logDir
79
- if (option.logFileName) LOG_FILE_NAME = option.logFileName
80
- LOG_FILE = LOG_DIR + '/' + LOG_FILE_NAME
81
- LOG_FILE_PREFIX = LOG_FILE_NAME.substring(0, LOG_FILE_NAME.lastIndexOf('.'))
82
- LOG_FILE_SUFFIX = LOG_FILE_NAME.substring(LOG_FILE_NAME.lastIndexOf('.') + 1)
109
+ if (option.logDir) this.LOG_DIR = option.logDir
110
+ if (option.logFileName) this.LOG_FILE_NAME = option.logFileName
111
+ this.LOG_FILE = this.LOG_DIR + '/' + this.LOG_FILE_NAME
112
+ this.LOG_FILE_PREFIX = this.LOG_FILE_NAME.substring(0, this.LOG_FILE_NAME.lastIndexOf('.'))
113
+ this.LOG_FILE_SUFFIX = this.LOG_FILE_NAME.substring(this.LOG_FILE_NAME.lastIndexOf('.') + 1)
83
114
 
84
115
  this.file = {
85
116
  level: option.level || 'off',
86
117
  logSize: 0,
87
- maxSize: option.maxSize ? option.maxSize * 1024 * 1024 : MAX_LOG_SIZE,
88
- maxFiles: option.maxFiles || MAX_LOG_FILES,
118
+ maxSize: option.maxSize ? option.maxSize * 1024 * 1024 : this.MAX_LOG_SIZE,
119
+ maxFiles: option.maxFiles || this.MAX_LOG_FILES,
89
120
  }
90
121
  } else if (option.type === 'console') {
91
122
  if (option.colorize === undefined) {
@@ -117,12 +148,12 @@ export class Logger {
117
148
  this.logChannel = diagnostics_channel.channel(this.category)
118
149
 
119
150
  if (this.file && LEVEL_TO_INT[this.file.level] > 0) {
120
- if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true })
121
- else if (existsSync(LOG_FILE)) this.rotateExistingLog()
151
+ if (!existsSync(this.LOG_DIR)) mkdirSync(this.LOG_DIR, { recursive: true })
152
+ else if (existsSync(this.LOG_FILE)) this.rotateExistingLog()
122
153
  if (typeof Bun !== 'undefined') { // Bun
123
- this.logStream = Bun.file(LOG_FILE).writer({ highWaterMark: HIGH_WATER_MARK })
154
+ this.logStream = Bun.file(this.LOG_FILE).writer({ highWaterMark: this.HIGH_WATER_MARK })
124
155
  } else { // Node.js
125
- this.logStream = createWriteStream(LOG_FILE, { flags: 'a' })
156
+ this.logStream = createWriteStream(this.LOG_FILE, { flags: 'a' })
126
157
  }
127
158
  this.subscribe(this.logToFile)
128
159
  }
@@ -151,8 +182,8 @@ export class Logger {
151
182
 
152
183
  private async rotateExistingLog() {
153
184
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
154
- const archivedFile = `${LOG_DIR}/${LOG_FILE_PREFIX}-${timestamp}.${LOG_FILE_SUFFIX}`
155
- renameSync(LOG_FILE, archivedFile)
185
+ const archivedFile = `${this.LOG_DIR}/${this.LOG_FILE_PREFIX}-${timestamp}.${this.LOG_FILE_SUFFIX}`
186
+ renameSync(this.LOG_FILE, archivedFile)
156
187
  this.cleanupOldLogs()
157
188
  }
158
189
 
@@ -165,9 +196,9 @@ export class Logger {
165
196
  }
166
197
  await this.rotateExistingLog()
167
198
  if (typeof Bun !== 'undefined') {
168
- this.logStream = Bun.file(LOG_FILE).writer({ highWaterMark: HIGH_WATER_MARK })
199
+ this.logStream = Bun.file(this.LOG_FILE).writer({ highWaterMark: this.HIGH_WATER_MARK })
169
200
  } else {
170
- this.logStream = createWriteStream(LOG_FILE, { flags: 'a' })
201
+ this.logStream = createWriteStream(this.LOG_FILE, { flags: 'a' })
171
202
  }
172
203
  this.flushBuffer()
173
204
  } catch (error) {
@@ -179,12 +210,12 @@ export class Logger {
179
210
 
180
211
  private cleanupOldLogs() {
181
212
  if (!this.file) return
182
- const logFiles = readdirSync(LOG_DIR)
183
- .filter(file => file.startsWith(`${LOG_FILE_PREFIX}-`) && file.endsWith(`.${LOG_FILE_SUFFIX}`))
213
+ const logFiles = readdirSync(this.LOG_DIR)
214
+ .filter(file => file.startsWith(`${this.LOG_FILE_PREFIX}-`) && file.endsWith(`.${this.LOG_FILE_SUFFIX}`))
184
215
  .sort((a, b) => b.localeCompare(a))
185
216
 
186
217
  if (logFiles.length > this.file.maxFiles) {
187
- logFiles.slice(this.file.maxFiles).forEach(file => unlinkSync(join(LOG_DIR, file)))
218
+ logFiles.slice(this.file.maxFiles).forEach(file => unlinkSync(join(this.LOG_DIR, file)))
188
219
  }
189
220
  }
190
221
 
@@ -867,7 +867,7 @@ export class ScimGateway {
867
867
  controller.enqueue(encoder.encode(`\u200B`)) // invisible keep-alive
868
868
  }, 10000)
869
869
 
870
- function cleanup() {
870
+ const cleanup = () => {
871
871
  clearInterval(keepAliveInterval)
872
872
  logger.unsubscribe(sub)
873
873
  controller.close()
@@ -2649,13 +2649,13 @@ export class ScimGateway {
2649
2649
  }
2650
2650
 
2651
2651
  const gracefulShutdown = async function () {
2652
+ logger.info(`${gwName}[${pluginName}] now stopping...`)
2653
+ await logger.close()
2652
2654
  if (server) {
2653
2655
  if (typeof Bun !== 'undefined') {
2654
2656
  server.stop(true)
2655
2657
  }
2656
2658
  }
2657
- logger.debug(`${gwName}[${pluginName}] received terminate/kill signal - closing connections and exit`)
2658
- logger.close()
2659
2659
  if (server) {
2660
2660
  if (typeof Bun !== 'undefined') {
2661
2661
  await Bun.sleep(400) // give in-flight requests a chance to complete, also plugins may use SIGTERM/SIGINT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.2.0",
3
+ "version": "5.2.1",
4
4
  "type": "module",
5
5
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
6
6
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",