samanbayaka 0.0.14 → 0.0.16

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
@@ -35,7 +35,7 @@ Once the package is installed, you can import the library using import or requir
35
35
  import sbk from "samanbayaka"
36
36
  ```
37
37
  # Prerequisites
38
- It is assumed that NATS, Redpanda, and Redis are installed and properly configured. All services should be discoverable through the /etc/hosts file.
38
+ It is assumed that NATS, Redpanda, Redis, and Etcd are installed and properly configured. All services should be discoverable through the /etc/hosts file.
39
39
  #### Minimum required Node.js version: >= 22.x.x
40
40
  ```bash
41
41
  node -v
@@ -46,7 +46,8 @@ Additionally, OpenObserve is used to view logs and telemetry.
46
46
  #### Environment variables
47
47
  To configure all required environment variables, create a Bash file:
48
48
  ```bash
49
- sudo nano /etc/logrotate.d/sbk.sh
49
+ source /etc/profile.d/sbk.sh
50
+
50
51
  ```
51
52
  Paste this:
52
53
  ```bash
@@ -250,12 +251,12 @@ The Gateway is a critical service that exposes all endpoints as REST APIs. At le
250
251
  ```bash
251
252
  mkdir gateway
252
253
  cd gateway
253
- npm init -y
254
+ pnpm init
254
255
  pnpm install samanbayaka
255
256
  touch index.mjs
256
257
  ```
257
258
 
258
- Open index.mjs and paste the following:
259
+ Open `index.mjs` and paste the following:
259
260
  ```bash
260
261
  import sbk from "samanbayaka"
261
262
  await sbk.loadGatewayService()
@@ -265,7 +266,7 @@ await sbk.loadGatewayService()
265
266
  ```bash
266
267
  mkdir <your_service_name>
267
268
  cd <your_service_name>
268
- npm init -y
269
+ pnpm init
269
270
  pnpm install samanbayaka
270
271
  touch index.mjs
271
272
  ```
@@ -274,12 +275,12 @@ A feature service, such as `hello`, can be created as follows:
274
275
  ```bash
275
276
  mkdir hello
276
277
  cd hello
277
- npm init -y
278
+ pnpm init
278
279
  pnpm install samanbayaka
279
280
  touch index.mjs
280
281
  ```
281
282
 
282
- Open index.mjs and paste the following:
283
+ Open `index.mjs` and paste the following:
283
284
  ```bash
284
285
  import sbk from "samanbayaka"
285
286
  await sbk.loadFeatureService({
@@ -325,7 +326,7 @@ await sbk.loadFeatureService({
325
326
  ```bash
326
327
  mkdir <your_service_name>-<type>-<topic>
327
328
  cd <your_service_name>-<type>-<topic>
328
- npm init -y
329
+ pnpm init
329
330
  pnpm install samanbayaka
330
331
  touch index.mjs
331
332
  ```
@@ -333,12 +334,12 @@ An auxiliary service, such as a `producer` that publishes messages to the `STUDE
333
334
  ```js
334
335
  mkdir producer-kafka-student
335
336
  cd producer-kafka-student
336
- npm init -y
337
+ pnpm init
337
338
  pnpm install samanbayaka
338
339
  touch index.mjs
339
340
  ```
340
341
 
341
- Open index.mjs and paste the following:
342
+ Open `index.mjs` and paste the following:
342
343
  ```bash
343
344
  import sbk from "samanbayaka"
344
345
  await sbk.auxBrokerService(
@@ -365,19 +366,19 @@ rest: false
365
366
  ```bash
366
367
  mkdir <your_service_name>-<type>-<topic>-<group>
367
368
  cd <your_service_name>-<type>-<topic>-<group>
368
- npm init -y
369
+ pnpm init
369
370
  pnpm install samanbayaka
370
371
  touch index.mjs
371
372
  ```
372
373
  An auxiliary service, such as a `consumer` that subscribe messages from the `STUDENT` topic having group name `junior`, can be created as follows:
373
374
  ```js
374
- mkdir producer-kafka-student-junior
375
- cd producer-kafka-student-junior
376
- npm init -y
375
+ mkdir consumer-kafka-student-junior
376
+ cd consumer-kafka-student-junior
377
+ pnpm init
377
378
  pnpm install samanbayaka
378
379
  touch index.mjs
379
380
  ```
380
- Open index.mjs and paste the following:
381
+ Open `index.mjs` and paste the following:
381
382
  ```bash
382
383
  import sbk from "samanbayaka"
383
384
  await sbk.auxBrokerService(
@@ -421,11 +422,11 @@ You can also run the demo service to better understand how microservices work in
421
422
  ```bash
422
423
  mkdir demo
423
424
  cd demo
424
- npm init -y
425
+ pnpm init
425
426
  pnpm install samanbayaka
426
427
  touch demo.mjs
427
428
  ```
428
- Open and edit the demo.mjs file as follows
429
+ Open and edit the `demo.mjs` file as follows
429
430
  ```bash
430
431
  import sbk from "samanbayaka"
431
432
  await sbk.loadDemo()
@@ -435,6 +436,10 @@ Run the service, demo
435
436
  node demo.mjs
436
437
  ```
437
438
 
439
+ ## Documentation
440
+
441
+ - [docker](docs/dockers.md)
442
+
438
443
  <p align="center" style="margin-top: 100px;">
439
444
  <img src="https://moleculer.services/images/banner.png" alt="Moleculer Logo" width="600">
440
445
  </p>
package/commit-hash.mjs CHANGED
@@ -1 +1 @@
1
- export const COMMIT_HASH = '782e7c8';
1
+ export const COMMIT_HASH = '6442e51';
@@ -24,7 +24,7 @@ const CONFIG_PATH = process.env.SBK_CONFIG_PATH // Environment variable of conf
24
24
  /**
25
25
  * Project absolute path
26
26
  */
27
- const ABSOLUTE_PATH = path.join(__filename.split('samanbayaka')[0], "samanbayaka")
27
+ const ABSOLUTE_PATH = path.join(__filename.split('/samanbayaka/')[0], "samanbayaka")
28
28
 
29
29
 
30
30
  /**
@@ -0,0 +1,171 @@
1
+ import ApiGateway from "moleculer-web"
2
+ import jwt from "jsonwebtoken"
3
+ import jwksClient from "jwks-rsa"
4
+
5
+ const OPENID_CONFIG = {
6
+ url: "https://accounts.google.com/.well-known/openid-configuration",
7
+ clientId: "184045176764-ugg28aegdro383pintufun14uubtt374.apps.googleusercontent.com",
8
+ jwksClient: {
9
+ cache: true,
10
+ cacheMaxEntries: 5,
11
+ cacheMaxAge: 10 * 60 * 1000,
12
+ rateLimit: true,
13
+ jwksRequestsPerMinute: 10,
14
+ },
15
+ jwtVerifyAlgo: ["RS256"],
16
+ client: {},
17
+ issuer: "",
18
+ }
19
+
20
+ // let client
21
+ // let issuer
22
+
23
+ /**
24
+ * Retrieves the public signing key corresponding to a JWT `kid`.
25
+ *
26
+ * Used by JWT verification libraries (such as `jsonwebtoken`)
27
+ * to dynamically resolve signing keys from a JWKS endpoint.
28
+ *
29
+ * @param {Object} header - Decoded JWT header.
30
+ * @param {string} header.kid - Key ID used to identify the signing key.
31
+ * @param {Function} callback - Callback function invoked after key retrieval.
32
+ * @param {Error|null} callback.err - Error object if key retrieval fails.
33
+ * @param {string|null} callback.key - PEM formatted public key.
34
+ *
35
+ * @returns {void}
36
+ */
37
+ const getKey = (header, callback) => {
38
+
39
+ OPENID_CONFIG.client.getSigningKey(
40
+ header.kid,
41
+ (err, key) => {
42
+
43
+ if (err) {
44
+ callback(err)
45
+ return
46
+ }
47
+
48
+ callback(
49
+ null,
50
+ key.publicKey || key.rsaPublicKey
51
+ )
52
+ }
53
+ )
54
+
55
+ }
56
+
57
+
58
+ /**
59
+ * Initializes OpenID Connect configuration and JWKS client.
60
+ *
61
+ * Fetches the OpenID configuration document from the configured
62
+ * issuer endpoint and creates a cached JWKS client for JWT
63
+ * signature verification.
64
+ *
65
+ * The JWKS client supports:
66
+ * - Automatic key retrieval
67
+ * - In-memory caching
68
+ * - Request rate limiting
69
+ *
70
+ * @async
71
+ * @function initOpenId
72
+ *
73
+ * @throws {Error} Throws if the OpenID configuration request fails
74
+ * or the response cannot be parsed.
75
+ *
76
+ * @returns {Promise<void>}
77
+ */
78
+ export const initOpenId = async () => {
79
+
80
+ const response = await fetch(OPENID_CONFIG.url)
81
+
82
+ if ( !response.ok ) {
83
+ throw new Error(
84
+ `OpenID configuration endpoint failed with status "${response.status}"`
85
+ )
86
+ }
87
+
88
+ const config = await response.json()
89
+
90
+ OPENID_CONFIG.issuer = config.issuer
91
+
92
+ OPENID_CONFIG.client = jwksClient({
93
+ ...OPENID_CONFIG.jwksClient,
94
+ ...{jwksUri: config.jwks_uri},
95
+ })
96
+
97
+ }
98
+
99
+
100
+ /**
101
+ * Authorizes the requester by validating the access token.
102
+ * The token can be provided either in the Bearer Authorization header
103
+ * or in cookies.
104
+ *
105
+ * @param {Context} ctx - Moleculer service context
106
+ * @param {Object} route - REST route configuration object
107
+ * @param {HTTP request<Object>} req - HTTP request object
108
+ * @returns {Promise<Object>} Authenticated user / authorization result
109
+ */
110
+ export const authorize = async (ctx, route, req) => {
111
+
112
+ let token = null
113
+
114
+ const auth =
115
+ req.headers.authorization
116
+
117
+ if (auth?.startsWith("Bearer ")) {
118
+ token = auth.replace(/^Bearer\s+/i, "")
119
+ }
120
+
121
+
122
+ if (
123
+ !token &&
124
+ req.cookies?.id_token
125
+ ) {
126
+ token = req.cookies.id_token
127
+ }
128
+
129
+ if (!token) {
130
+ throw new ApiGateway.Errors.UnAuthorizedError(
131
+ ApiGateway.Errors.ERR_NO_TOKEN
132
+ )
133
+ }
134
+
135
+ try {
136
+
137
+ const decoded =
138
+ await new Promise((resolve, reject) => {
139
+
140
+ jwt.verify(
141
+ token,
142
+ getKey,
143
+ {
144
+ algorithms: OPENID_CONFIG.jwtVerifyAlgo,
145
+ issuer: OPENID_CONFIG.issuer,
146
+ audience: OPENID_CONFIG.clientId,
147
+ },
148
+
149
+ (err, decoded) => {
150
+
151
+ if (err) {
152
+ reject(err)
153
+ return
154
+ }
155
+
156
+ resolve(decoded)
157
+ }
158
+ )
159
+ })
160
+
161
+ ctx.broker.logger.debug({message: "Decoded Token Object", token: decoded})
162
+ ctx.meta.user = { name: decoded?.name, email: decoded?.email }
163
+ return decoded
164
+
165
+ } catch (err) {
166
+ throw new ApiGateway.Errors.UnAuthorizedError(
167
+ ApiGateway.Errors.ERR_INVALID_TOKEN
168
+ )
169
+ }
170
+ }
171
+
package/index.mjs CHANGED
@@ -19,7 +19,6 @@ import HybridCacher from "#hMol/HybridCacher.mjs"
19
19
  import about from '#sAbt/index.mjs'
20
20
  import apiGateway from '#sApi/index.mjs'
21
21
  import { auxBrokerParamsValidator } from '#hUti/aux-broker-params-validator.mjs'
22
-
23
22
  import * as kafka from '#sKfk/index.mjs'
24
23
  import * as demo from '#sDmo/index.mjs'
25
24
 
@@ -27,7 +26,6 @@ import * as demo from '#sDmo/index.mjs'
27
26
  const BROKER_CONFIG = await getConfig('nats')
28
27
  const CATCHER_CONFIG = await getConfig('catcher')
29
28
  const TELEMETRY_CONFIG = await getConfig('telemetry')
30
- const KAFKA_CONFIG = await getConfig('kafkajs')
31
29
 
32
30
  const sbkLoggers = {
33
31
  "CustomPino": customLogger
@@ -155,7 +153,7 @@ export default {
155
153
  auxBroker = kafka
156
154
  }
157
155
  else {
158
- throw new Error("The \"type\" value must be either \"kafka\" or \"mqtt\".")
156
+ throw new Error("The \"type\" value must be either \"kafka\" or \"mqtt\" or \"amqp\"")
159
157
  }
160
158
 
161
159
  opts.logLevel = logLevel
@@ -179,7 +177,6 @@ export default {
179
177
  )
180
178
  }
181
179
 
182
-
183
180
  /**
184
181
  * Start broker
185
182
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "samanbayaka",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Moleculer Gateway service with kafka transporter",
5
5
  "homepage": "https://gitlab.com/dalal.suvendu/samanbayaka#readme",
6
6
  "bugs": {
@@ -43,6 +43,8 @@
43
43
  "cookie-parser": "^1.4.7",
44
44
  "helmet": "^8.1.0",
45
45
  "ioredis": "^5.10.1",
46
+ "jsonwebtoken": "^9.0.3",
47
+ "jwks-rsa": "^4.0.1",
46
48
  "kafkajs": "^2.2.4",
47
49
  "lru-cache": "^11.3.5",
48
50
  "moleculer": "0.15.0",
@@ -0,0 +1,93 @@
1
+ <button onclick="login()">Login with Google</button>
2
+ <button onclick="callAPI()">Call API</button>
3
+
4
+ <script>
5
+ const CLIENT_ID = "184045176764-fmdarpud3fkpojo5p73mloetgrqrg65p.apps.googleusercontent.com";
6
+ const REDIRECT_URI = "http://localhost:3000";
7
+ const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
8
+ const TOKEN_URL = "https://oauth2.googleapis.com/token";
9
+
10
+ // --- PKCE helpers ---
11
+ function generateRandomString(length) {
12
+ const chars =
13
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
14
+ let result = "";
15
+ for (let i = 0; i < length; i++) {
16
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
17
+ }
18
+ return result;
19
+ }
20
+
21
+ async function sha256(verifier) {
22
+ const encoder = new TextEncoder();
23
+ const data = encoder.encode(verifier);
24
+ const hash = await crypto.subtle.digest("SHA-256", data);
25
+ return btoa(String.fromCharCode(...new Uint8Array(hash)))
26
+ .replace(/\+/g, "-")
27
+ .replace(/\//g, "_")
28
+ .replace(/=+$/, "");
29
+ }
30
+
31
+ // --- Step 1: login redirect ---
32
+ async function login() {
33
+ const codeVerifier = generateRandomString(64);
34
+ const codeChallenge = await sha256(codeVerifier);
35
+
36
+ localStorage.setItem("code_verifier", codeVerifier);
37
+
38
+ const url =
39
+ `${AUTH_URL}?response_type=code` +
40
+ `&client_id=${CLIENT_ID}` +
41
+ `&redirect_uri=${REDIRECT_URI}` +
42
+ `&scope=openid%20email%20profile` +
43
+ `&code_challenge=${codeChallenge}` +
44
+ `&code_challenge_method=S256`;
45
+
46
+ window.location = url;
47
+ }
48
+
49
+ // --- Step 2: handle redirect ---
50
+ async function handleRedirect() {
51
+ const params = new URLSearchParams(window.location.search);
52
+ const code = params.get("code");
53
+
54
+ if (!code) return;
55
+
56
+ const codeVerifier = localStorage.getItem("code_verifier");
57
+
58
+ const res = await fetch(TOKEN_URL, {
59
+ method: "POST",
60
+ headers: {
61
+ "Content-Type": "application/x-www-form-urlencoded",
62
+ },
63
+ body: new URLSearchParams({
64
+ client_id: CLIENT_ID,
65
+ grant_type: "authorization_code",
66
+ code,
67
+ code_verifier: codeVerifier,
68
+ redirect_uri: REDIRECT_URI,
69
+ }),
70
+ });
71
+
72
+ const data = await res.json();
73
+
74
+ console.log("Access Token:", data.access_token);
75
+
76
+ localStorage.setItem("access_token", data.access_token);
77
+ }
78
+
79
+ // --- Step 3: call API ---
80
+ async function callAPI() {
81
+ const token = localStorage.getItem("access_token");
82
+
83
+ const res = await fetch("http://localhost:4000/api/resource", {
84
+ headers: {
85
+ Authorization: `Bearer ${token}`,
86
+ },
87
+ });
88
+
89
+ console.log(await res.json());
90
+ }
91
+
92
+ handleRedirect();
93
+ </script>
@@ -3,7 +3,7 @@ import { pkgDtls } from '#hFil/esm-loading.mjs'
3
3
  export default {
4
4
  name: "about",
5
5
  actions: {
6
- me: {
6
+ project: {
7
7
  rest: {
8
8
  method: "GET",
9
9
  path: "/"
@@ -34,5 +34,15 @@ export default {
34
34
  return `${pkgDtls.fullName} ${pkgDtls.version}`
35
35
  },
36
36
  },
37
+ profile: {
38
+ rest: {
39
+ method: "GET",
40
+ path: "/me"
41
+ },
42
+
43
+ handler: async(ctx) => {
44
+ return ctx.meta.user
45
+ }
46
+ }
37
47
  },
38
48
  }
@@ -0,0 +1,37 @@
1
+ import { pkgDtls } from '#hFil/esm-loading.mjs'
2
+
3
+ export default {
4
+ name: "bff",
5
+ actions: {
6
+ signin: {
7
+ rest: {
8
+ method: "GET",
9
+ path: "/signin"
10
+ },
11
+
12
+ handler: async(ctx) => {
13
+ return `Welcome to ${pkgDtls.name} is a microservices-based project built with moleculer and moleculer-web, designed for scalable, high-performance service communication and API management.`
14
+ },
15
+ },
16
+ callback: {
17
+ rest: {
18
+ method: "GET",
19
+ path: "/callback"
20
+ },
21
+
22
+ handler: async(ctx) => {
23
+ return `${pkgDtls.fullName} ${pkgDtls.version}`
24
+ },
25
+ },
26
+ signout: {
27
+ rest: {
28
+ method: "GET",
29
+ path: "/signout"
30
+ },
31
+
32
+ handler: async(ctx) => {
33
+ return ctx.meta.user
34
+ }
35
+ }
36
+ },
37
+ }
@@ -1,4 +1,4 @@
1
- /*index.mjs*/
1
+ // index.mjs
2
2
  import ApiGateway from "moleculer-web"
3
3
  import OpenApi from "moleculer-auto-openapi"
4
4
  import cookieParser from "cookie-parser"
@@ -6,6 +6,7 @@ import helmet from "helmet"
6
6
  import compression from "compression"
7
7
 
8
8
  import {formatHttpErrors} from '#hUti/error-handler.mjs'
9
+ import { initOpenId, authorize } from '#hUti/access-token-validator.mjs'
9
10
 
10
11
  export default {
11
12
  name: "gateway",
@@ -13,6 +14,13 @@ export default {
13
14
  ApiGateway,
14
15
  ],
15
16
 
17
+ async started() {
18
+ /**
19
+ * Initialize OpenID config
20
+ */
21
+ await initOpenId()
22
+ },
23
+
16
24
  settings: {
17
25
  port: process.env.SBK_PORT || 8765,
18
26
 
@@ -42,14 +50,33 @@ export default {
42
50
 
43
51
 
44
52
  routes: [
53
+ // Public routes
54
+ {
55
+ path: "/",
56
+ authorization: false,
57
+
58
+ whitelist: [
59
+ "bff.*",
60
+ ],
61
+
62
+ autoAliases: true,
63
+ mappingPolicy: "restrict",
64
+
65
+ bodyParsers: {
66
+ json: true,
67
+ urlencoded: { extended: true }
68
+ }
69
+ },
70
+
45
71
  /**
46
- * API routes
72
+ * Protected API routes
47
73
  */
48
74
  {
49
75
  path: "/api",
76
+ authorization: true,
50
77
  whitelist: [
51
78
  /**
52
- * Access any actions except 'system' service
79
+ * Access any actions except 'gateway' and 'system' service
53
80
  */
54
81
  /^(?!(gateway|system)\.)\w+(?:\.\w+)*$/
55
82
  ],
@@ -124,6 +151,7 @@ export default {
124
151
  // this.logger.error(" Request error!", err.name, ":", err.message, "\n", err.stack, "\nData:", err.data);
125
152
  // }
126
153
  this.sendError(req, res, err)
127
- }
154
+ },
155
+ authorize,
128
156
  },
129
157
  }
@@ -196,7 +196,7 @@ export const consumer = (opts, callback) => {
196
196
 
197
197
  if(interMessageDelayMs > 0 ){
198
198
  // control message reading per second
199
- await delay(1000)
199
+ await delay(interMessageDelayMs)
200
200
  }
201
201
  }
202
202