@samanbayaka/core 0.0.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.
@@ -0,0 +1 @@
1
+ export const COMMIT_HASH = '125c4ea';
@@ -0,0 +1,90 @@
1
+ /*AjvValidator.mjs*/
2
+
3
+ import Ajv from "ajv"
4
+ import addFormats from "ajv-formats"
5
+ import { Validators, Errors } from "moleculer"
6
+
7
+ const ajv = new Ajv({
8
+ allErrors: true,
9
+ strict: false,
10
+ coerceTypes: true //true auto fit data to integer data type like 12 if supplied "12"
11
+ })
12
+
13
+ addFormats(ajv)
14
+
15
+ export class AjvValidator extends Validators.Base {
16
+ constructor() {
17
+ super()
18
+ this.ajv = ajv
19
+ }
20
+
21
+ compile(schema) {
22
+ const validate = this.ajv.compile(schema)
23
+
24
+ return (params) => {
25
+ const valid = validate(params)
26
+
27
+ if (!valid) {
28
+ return formatAjvErrors(validate.errors, params)
29
+ }
30
+ return true
31
+ }
32
+ }
33
+ }
34
+
35
+
36
+ export const validateOAS = (params, schema) => {
37
+ const validate = ajv.compile(schema)
38
+ const valid = validate(params)
39
+ if (!valid) {
40
+ throw new Errors.ValidationError(
41
+ "Validation failed",
42
+ "VALIDATION_ERROR",
43
+ formatAjvErrors(validate.errors, params)
44
+ )
45
+ }
46
+ return valid
47
+ }
48
+
49
+
50
+ /**
51
+ * Format AJV Errors
52
+ * @param {error object}
53
+ * @param {params object}
54
+ * @return {array}
55
+ */
56
+ const formatAjvErrors = (errors, params) => {
57
+ if(!errors){
58
+ return false
59
+ }
60
+
61
+ return errors.map(err => {
62
+
63
+ /**
64
+ * Remove leading "/" and subsequent "/" to "."
65
+ * @type {string}
66
+ */
67
+ const path = err.instancePath
68
+ .replace(/^\/+/, '')
69
+ .replace(/\//g, '.')
70
+
71
+ /**
72
+ * Getting value from nested object
73
+ * @param {string} like query.name.lastName
74
+ * @return {string}
75
+ */
76
+ const val = path.split('.').reduce((acc, key) => acc?.[key], params)
77
+
78
+ if (err.keyword === "required") {
79
+ return {
80
+ path,
81
+ message: `Must have required property '${err.params.missingProperty}' in the path object.`
82
+ }
83
+ }
84
+ return {
85
+ path,
86
+ message: `The value '${val}' ${err.message}`
87
+ }
88
+ })
89
+
90
+ }
@@ -0,0 +1,83 @@
1
+ import pino from "pino"
2
+
3
+ const pinoOptions = {
4
+ /**
5
+ * ISO timestamps
6
+ */
7
+ timestamp: pino.stdTimeFunctions.isoTime,
8
+
9
+ /**
10
+ * base fields = Loki labels candidates
11
+ */
12
+ base: null,
13
+
14
+ /**
15
+ * Rename default fields (optional but cleaner for Loki)
16
+ * @type {Object}
17
+ */
18
+ // formatters: {
19
+ // level(label) {
20
+ // return { level: label } // instead of numeric
21
+ // }
22
+ // },
23
+
24
+ serializers: {
25
+ err: pino.stdSerializers.err
26
+ },
27
+
28
+ // messageKey: "message",
29
+ nestedKey: 'data',
30
+ // msgPrefix: " ❖❖ SBK ❖❖ ",
31
+ hooks: {
32
+ logMethod(args, method, level) {
33
+ let mergedObj = {tag: "MOL", msgHdr: " ━━━━━━━❖❖ SBK ❖❖━━━━━━━ ", message: ""}
34
+ let messages = []
35
+ let tag
36
+
37
+ for (const arg of args) {
38
+ if (typeof arg === 'string') {
39
+ messages.push(arg.replace(/\u001b\[[0-9;]*m/g, " "))
40
+ }
41
+ else if (arg && typeof arg === 'object') {
42
+ if( arg.message ){
43
+ arg.message = typeof arg.message === 'object'
44
+ ? JSON.stringify(arg.message)
45
+ : arg.message
46
+ messages.push(arg.message)
47
+ }
48
+ mergedObj = { ...mergedObj, ...arg }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Delete message header when tag is not "SBK"
54
+ */
55
+ if( !(mergedObj?.tag == "SBK") ){
56
+ delete mergedObj.msgHdr
57
+ }
58
+
59
+ /**
60
+ * Rebuild args into standard Pino format
61
+ */
62
+ args = [{...mergedObj, message: messages.join(' ')}]
63
+
64
+ method.apply(this, args)
65
+ }
66
+ },
67
+ }
68
+
69
+
70
+ export default (opts) => {
71
+ const { logger, logLevel } = opts
72
+ return logger == "CustomLogger"
73
+ ? {
74
+ type: "Pino",
75
+ options: {
76
+ level: (logLevel || "info").toLowerCase(),
77
+ pino: {
78
+ options: pinoOptions
79
+ }
80
+ }
81
+ }
82
+ : "Console"
83
+ }
@@ -0,0 +1,155 @@
1
+ import { Cachers } from "moleculer"
2
+
3
+ const MAX_L1_TTL = 600 // 10 minutes in seconds
4
+ const MAX_L2_TTL = 8640_000 // 100 days in seconds
5
+ const MAX_DATA_SZ = 10*1024*1024 // maximum data size in byte
6
+
7
+ export default class HybridCacher extends Cachers.Base {
8
+ constructor(opts = {}) {
9
+ super(opts)
10
+
11
+ this.memory = new Cachers.MemoryLRU({
12
+ max: opts.max || 1000,
13
+ ttl: opts.ttl[1]
14
+ })
15
+
16
+ this.redis = new Cachers.Redis({
17
+ ttl: opts.ttl[0],
18
+ redis: opts.redis
19
+ })
20
+ }
21
+
22
+ async init(broker) {
23
+ this.logger = broker.logger
24
+ await this.memory.init(broker)
25
+ await this.redis.init(broker)
26
+ }
27
+
28
+
29
+ /**
30
+ * Helper to extract key prifix used by redis
31
+ * @param {string} key
32
+ * @return {string}
33
+ */
34
+ getRedisCatchKey(key) {
35
+ return this.redis.prefix ? this.redis.prefix + key : key
36
+ }
37
+
38
+ async get(key) {
39
+ const prefixedKey = this.getRedisCatchKey(key)
40
+
41
+ /**
42
+ * Try L1 first
43
+ * @type {object | string}
44
+ */
45
+ let data = await this.memory.get(prefixedKey)
46
+ if (data) {
47
+ this.logger.debug({tag: "SBK", message: 'L1 Hits'})
48
+ return data
49
+ }
50
+
51
+ /**
52
+ * Fallback to Redis
53
+ * @type {object | string}
54
+ */
55
+ data = await this.redis.get(key)
56
+ if (data) {
57
+ /**
58
+ * Warm L1 using the TTL of L1 and the remaining time to L2, whichever is lower.
59
+ */
60
+ const l1WarmTTL = Math.min((data.ttl[0] - ( Date.now() - data.txnTs)/1000), data.ttl[1])
61
+ this.logger.debug({tag: "SBK", message: 'L2 Hits'})
62
+ if( l1WarmTTL > 0 ) {
63
+ await this.memory.set(prefixedKey, data.result, l1WarmTTL)
64
+ }
65
+ }
66
+
67
+ return data?.result
68
+ }
69
+
70
+ async set(key, data, ttl, meta = {}) {
71
+ const prefixedKey = this.getRedisCatchKey(key)
72
+
73
+ const dataSize = ((szData = this.redis.serializer.serialize(data)) => {
74
+ return Buffer.isBuffer(szData)
75
+ ? szData.length
76
+ : Buffer.byteLength(szData)
77
+ })()
78
+
79
+ if(dataSize > MAX_DATA_SZ){
80
+ this.logger.error({
81
+ tag: "SBK",
82
+ message: `Cache data size is '${dataSize}' bytes exceeds the configured limit`
83
+ })
84
+ return
85
+ }
86
+
87
+ /**
88
+ * Normalize global TTL
89
+ * @type {[number]}
90
+ */
91
+ const globalTTL = Array.isArray(this.opts.ttl)
92
+ ? [this.opts.ttl[0], this.opts.ttl[1] || 0]
93
+ : [this.opts.ttl || 0, 0]
94
+
95
+ /**
96
+ * Normalize incoming TTL
97
+ */
98
+ let L1TTL = 0
99
+ let L2TTL = 0
100
+
101
+ if (Array.isArray(ttl)) {
102
+ L2TTL = ttl[0]
103
+ L1TTL = ttl[1] || 0
104
+ } else if (typeof ttl === "number") {
105
+ L2TTL = ttl
106
+ }
107
+
108
+ const safeL2TTL = Math.min(L2TTL, globalTTL[0], MAX_L2_TTL)
109
+ const safeL1TTL = Math.min(L1TTL, globalTTL[1], MAX_L1_TTL)
110
+
111
+ /**
112
+ * Store in Redis (L2) when safeL2TTL > 0 and safeL2TTL > safeL1TTL
113
+ */
114
+ if( safeL2TTL > 0 && safeL2TTL > safeL1TTL ){
115
+ await this.redis.set(key, {result:data, ttl:[safeL2TTL, safeL1TTL], txnTs: Date.now()}, safeL2TTL)
116
+ }
117
+
118
+ /**
119
+ * Only store in Memory if l1TTL > 0
120
+ */
121
+ if (safeL1TTL > 0) {
122
+ await this.memory.set(prefixedKey, data, safeL1TTL)
123
+ }
124
+ }
125
+
126
+ async del(keys) {
127
+ await this.redis.del(keys)
128
+ await this.memory.del(keys)
129
+
130
+ }
131
+
132
+ async clean(match) {
133
+ await this.redis.clean(match)
134
+
135
+ let matchs = Array.isArray(match) ? match : [match]
136
+ matchs = matchs.map(el => el.replace("*", ""))
137
+
138
+ for (const patt of matchs){
139
+ const mKeys = this.memory.cache.keys()
140
+ let key = mKeys.next()
141
+ while (!key.done) {
142
+ if ( key.value.includes(patt) ) {
143
+ this.memory.cache.delete(key.value)
144
+ this.logger.debug({
145
+ tag: "SBK",
146
+ message: "REMOVE MEMORY LRU KEY",
147
+ key
148
+ })
149
+ }
150
+ key = mKeys.next()
151
+ }
152
+ }
153
+
154
+ }
155
+ }
@@ -0,0 +1,249 @@
1
+ import { Errors } from "moleculer"
2
+ import ApiGateway from "moleculer-web"
3
+ import jwt from "jsonwebtoken"
4
+ import jwksClient from "jwks-rsa"
5
+
6
+ import * as configHdl from '#hUti/config-handler.mjs'
7
+
8
+ const ACCESS_ROLES = await configHdl.getConfigs('/config/sbk/edge/yaml/roles')
9
+ const OPENID_CONFIG = await configHdl.getConfigs('/config/sbk/edge/yaml/auth')
10
+ export const {clientId} = OPENID_CONFIG
11
+
12
+ /**
13
+ * Initializes OpenID Connect configuration and JWKS client.
14
+ *
15
+ * Fetches the OpenID configuration document from the configured
16
+ * issuer endpoint and creates a cached JWKS client for JWT
17
+ * signature verification.
18
+ *
19
+ * The JWKS client supports:
20
+ * - Automatic key retrieval
21
+ * - In-memory caching
22
+ * - Request rate limiting
23
+ *
24
+ * @async
25
+ * @function getOpenidConfigs
26
+ *
27
+ * @throws {Error} Throws if the OpenID configuration request fails
28
+ * or the response cannot be parsed.
29
+ *
30
+ * @returns {Promise<void>}
31
+ */
32
+ const getOpenidConfigs = async () => {
33
+
34
+ const response = await fetch(OPENID_CONFIG.url)
35
+
36
+ if ( !response.ok ) {
37
+ throw new Error(
38
+ `OpenID configuration endpoint failed with status "${response.status}"`
39
+ )
40
+ }
41
+
42
+ return await response.json()
43
+ }
44
+
45
+ export const {
46
+ issuer,
47
+ jwks_uri: jwksUri,
48
+ authorization_endpoint: authorizationEndpoint,
49
+ token_endpoint: tokenEndpoint,
50
+ end_session_endpoint: endSessionEndpoint,
51
+ } = await getOpenidConfigs()
52
+
53
+ const client = jwksClient({
54
+ ...OPENID_CONFIG.jwksClient,
55
+ jwksUri: jwksUri,
56
+ })
57
+
58
+ const issuers = ((iss) => {
59
+ const httpUrl = new URL(iss)
60
+ httpUrl.protocol = "http:"
61
+ httpUrl.port = String(OPENID_CONFIG.kcPort)
62
+ return [
63
+ iss,
64
+ httpUrl.toString(),
65
+ ]
66
+ })(issuer)
67
+
68
+
69
+ /**
70
+ * Retrieves the public signing key corresponding to a JWT `kid`.
71
+ *
72
+ * Used by JWT verification libraries (such as `jsonwebtoken`)
73
+ * to dynamically resolve signing keys from a JWKS endpoint.
74
+ *
75
+ * @param {Object} header - Decoded JWT header.
76
+ * @param {string} header.kid - Key ID used to identify the signing key.
77
+ * @param {Function} callback - Callback function invoked after key retrieval.
78
+ * @param {Error|null} callback.err - Error object if key retrieval fails.
79
+ * @param {string|null} callback.key - PEM formatted public key.
80
+ *
81
+ * @returns {void}
82
+ */
83
+ const getKey = (header, callback) => {
84
+
85
+ client.getSigningKey(
86
+ header.kid,
87
+ (err, key) => {
88
+
89
+ if (err) {
90
+ callback(err)
91
+ return
92
+ }
93
+
94
+ callback(
95
+ null,
96
+ key.publicKey || key.rsaPublicKey
97
+ )
98
+ }
99
+ )
100
+
101
+ }
102
+
103
+
104
+
105
+ /**
106
+ * Authenticates the requester by verifying the access token.
107
+ * The token can be provided either in the Bearer Authorization header
108
+ * or in cookies.
109
+ *
110
+ * @param {Context} ctx - Moleculer service context
111
+ * @param {Object} route - REST route configuration object
112
+ * @param {HTTP request<Object>} req - HTTP request object
113
+ * @returns {Promise<Object>} Authenticated user / authorization result
114
+ */
115
+ export const authenticate = async (ctx, route, req, res) => {
116
+
117
+ let token = null
118
+
119
+ const auth =
120
+ req.headers.authorization
121
+
122
+ if (auth?.startsWith("Bearer ")) {
123
+ token = auth.replace(/^Bearer\s+/i, "")
124
+ }
125
+
126
+
127
+ if (
128
+ !token &&
129
+ req.cookies?.WbsedclAccessToken
130
+ ) {
131
+ token = req.cookies?.WbsedclAccessToken
132
+ }
133
+
134
+
135
+ if (!token) {
136
+ throw new ApiGateway.Errors.UnAuthorizedError(
137
+ ApiGateway.Errors.ERR_NO_TOKEN
138
+ )
139
+ }
140
+
141
+ try {
142
+ const decoded =
143
+ await new Promise((resolve, reject) => {
144
+
145
+ jwt.verify(
146
+ token,
147
+ getKey,
148
+ {
149
+ algorithms: OPENID_CONFIG.jwtVerifyAlgo,
150
+ issuers,
151
+ // audience: OPENID_CONFIG.audience,
152
+ },
153
+
154
+ (err, dec) => {
155
+
156
+ if (err) {
157
+ reject(err)
158
+ return
159
+ }
160
+
161
+ resolve(dec)
162
+ }
163
+ )
164
+ })
165
+
166
+ ctx.broker.logger.debug({tag: "SBK", message: "Decoded Token Object", token: decoded, OPENID_CONFIG, issuers})
167
+ // ctx.meta.user = {
168
+ // sessionId: decoded?.sid,
169
+ // id: decoded?.sub,
170
+ // userId: (decoded?.sub).split(":").at(-1),
171
+ // username: decoded?.preferred_username,
172
+ // firstName: decoded?.given_name,
173
+ // lastName: decoded?.family_name,
174
+ // fullName: [...new Set(decoded?.name.split(/\s+/))].join(" "),
175
+ // emailIds: decoded?.email,
176
+ // mobileNos: decoded?.mobile_no,
177
+ // userRoles: decoded?.user_role,
178
+ // }
179
+ return decoded
180
+
181
+ } catch (err) {
182
+ ctx.broker.logger.debug({tag: "SBK", OPENID_CONFIG})
183
+ throw new ApiGateway.Errors.UnAuthorizedError(
184
+ ApiGateway.Errors.ERR_INVALID_TOKEN
185
+ )
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Verifies the user's roles and access grants before authorizing
191
+ * access to the requested resource.
192
+ * @param {object} ctx [description]
193
+ * @param {object} route [description]
194
+ * @param {object} req [description]
195
+ * @param {object} res [description]
196
+ * @return {object} [description]
197
+ */
198
+ export const authorize = async(ctx, route, req, res) => {
199
+
200
+ ctx.broker.logger.debug({tag: "SBK", message: "User Profile"}, ctx.meta.user, route?.path)
201
+
202
+ if( ctx.meta?.user?.preferred_username === undefined ){
203
+ throw new ApiGateway.Errors.UnAuthorizedError(
204
+ ApiGateway.Errors.ERR_INVALID_TOKEN
205
+ )
206
+ }
207
+
208
+ ctx.broker.logger.debug({tag: "SBK", message: "ACCESS_ROLES", ACCESS_ROLES, resource_access: ctx.meta.user.resource_access})
209
+
210
+ // resource_access
211
+
212
+ // const aa = new RegExp("^/system/oidc(?:/.*)?$", "i")
213
+ // if()
214
+
215
+ // if( req.url.startsWith("/system" ){
216
+
217
+ // }
218
+
219
+
220
+ // /**
221
+ // * Resticed accessor can't access all resources
222
+ // */
223
+ // if( ctx.meta?.user?.preferred_username == OPENID_CONFIG.rAccessor ){
224
+ // throw new Errors.MoleculerError(
225
+ // "You are not authorized to access this resource",
226
+ // 403,
227
+ // "FORBIDDEN"
228
+ // )
229
+ // }
230
+
231
+ // if( ctx.meta.user?.azp == clientId && req.url.startsWith(OPENID_CONFIG.rRoute)) {
232
+ // throw new Errors.MoleculerError(
233
+ // "You are not authorized to access this resource",
234
+ // 403,
235
+ // "FORBIDDEN"
236
+ // )
237
+ // }
238
+
239
+ // if( ctx.meta.user?.azp == OPENID_CONFIG.rAccessor && !req.url.startsWith(OPENID_CONFIG.rRoute)) {
240
+ // throw new Errors.MoleculerError(
241
+ // "You are not authorized to access this resource",
242
+ // 403,
243
+ // "FORBIDDEN"
244
+ // )
245
+ // }
246
+
247
+ return Promise.resolve(ctx)
248
+ }
249
+
@@ -0,0 +1,126 @@
1
+ export const auxBrokerParamsValidator = (paramObj) => {
2
+
3
+ let serviceNamePrefix = "producer"
4
+ const optsKeys = Object.keys(paramObj.opts || {})
5
+
6
+ /**
7
+ * Callback function.
8
+ *
9
+ * @callback Callback
10
+ * @param {...*} args - Arguments passed to the callback.
11
+ * @returns {boolean} Returns `true` by default when the original callback returns `null` or `undefined`.
12
+ */
13
+ if (typeof paramObj.callback !== 'function') {
14
+ paramObj.callback = () => true
15
+ }
16
+ else {
17
+ const originalCallback = paramObj.callback
18
+
19
+ paramObj.callback = (...args) => {
20
+ return originalCallback(...args) ?? true
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Throws an error when type is undefined
26
+ */
27
+ if ( !paramObj.type ) {
28
+ throw new Error("The \"type\" argument is missing.")
29
+ }
30
+
31
+
32
+ /**
33
+ * Determines and assigns the service name based on the provided options.
34
+ *
35
+ * Behavior:
36
+ * - If no option keys are provided:
37
+ * - Sets the service type prefix to `"producer"`.
38
+ * - Automatically generates the service name using the format:
39
+ * `"producer-<type>"`.
40
+ *
41
+ * - If option keys are present:
42
+ * - Ensures that `opts.name` is explicitly provided.
43
+ * - Throws an error if the `name` key is missing.
44
+ * - Sets the service type prefix to `"consumer"` when the
45
+ * `"consumer"` option is detected.
46
+ *
47
+ * @throws {Error}
48
+ * Thrown when option keys are provided but the `name` key
49
+ * is missing in `paramObj.opts`.
50
+ */
51
+ if ( optsKeys.length === 0 ) {
52
+ serviceNamePrefix = "producer"
53
+ paramObj.opts.name = `${serviceNamePrefix}-${paramObj.type}`
54
+ }
55
+ else {
56
+ if( !("name" in paramObj.opts) ){
57
+ throw new Error("Missing required key \"name\" in optins.")
58
+ }
59
+ if ( optsKeys.includes("consumer") ) {
60
+ serviceNamePrefix = "consumer"
61
+ }
62
+ }
63
+
64
+
65
+ /**
66
+ * Builds and normalizes the service name and topic pattern
67
+ * from the provided service configuration.
68
+ *
69
+ * Processing steps:
70
+ * - Creates a unique name segment list using:
71
+ * - `serviceNamePrefix`
72
+ * - `paramObj.type`
73
+ * - Parts extracted from `paramObj.opts.name`
74
+ * - Removes duplicate segments using `Set`.
75
+ *
76
+ * Topic generation:
77
+ * - If more than two segments exist:
78
+ * - Uses segments after the first two as topic names.
79
+ * - Converts topic segments to uppercase.
80
+ * - Converts a trailing `*` wildcard into Kafka-style `.*`.
81
+ * - Joins topic parts using `"."`.
82
+ * - Otherwise:
83
+ * - Uses the default topic pattern `".*"`.
84
+ *
85
+ * Consumer-specific behavior:
86
+ * - When the service type is `"consumer"`:
87
+ * - Appends `consumer.groupId` to the final service name.
88
+ * - Throws an error if `groupId` is missing.
89
+ *
90
+ * Final name normalization:
91
+ * - Joins all name segments using `"-"`.
92
+ * - Removes trailing `*` and trailing `-` characters.
93
+ *
94
+ * @throws {Error}
95
+ * Thrown when the service is a consumer and
96
+ * `consumer.groupId` is not defined.
97
+ */
98
+ const splitName = [ ...new Set(
99
+ [ ...[serviceNamePrefix, paramObj.type],
100
+ ...paramObj.opts.name.split("-"),
101
+ ])
102
+ ]
103
+
104
+ if( splitName.length > 2 ) {
105
+ const topics = splitName.slice(2).map(e => e.toUpperCase())
106
+ topics[topics.length - 1] = topics.at(-1)?.replace(/\*$/, ".*")
107
+ paramObj.opts.topicRegx = topics.join(".")
108
+ }
109
+ else{
110
+ paramObj.opts.topicRegx = '.*'
111
+ }
112
+
113
+ paramObj.opts.name = splitName.join("-")
114
+ .replace(/\*$/, "")
115
+ .replace(/\-$/, "")
116
+
117
+ if( splitName[0] == "consumer" ) {
118
+ if( paramObj.opts?.consumer?.groupId !== undefined ) {
119
+ paramObj.opts.name = `${paramObj.opts.name}-${paramObj.opts?.consumer?.groupId}`
120
+ }
121
+ else {
122
+ throw new Error("The \"groupId\" key is missing in the consumer options.")
123
+ }
124
+ }
125
+
126
+ }