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 +15 -10
- package/lib/logger.ts +58 -27
- package/lib/scimgateway.ts +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,15 +15,14 @@ Validated through IdP's:
|
|
|
15
15
|
- SailPoint/IdentityNow
|
|
16
16
|
|
|
17
17
|
Latest news:
|
|
18
|
-
|
|
19
|
-
-
|
|
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
|
|
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 -
|
|
739
|
-
We may
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/lib/scimgateway.ts
CHANGED
|
@@ -867,7 +867,7 @@ export class ScimGateway {
|
|
|
867
867
|
controller.enqueue(encoder.encode(`\u200B`)) // invisible keep-alive
|
|
868
868
|
}, 10000)
|
|
869
869
|
|
|
870
|
-
|
|
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