biz-a-cli 2.3.43 → 2.3.53

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,57 @@
1
+ import axios from 'axios'
2
+ import { IDLE_SOCKET_TIMEOUT_MILLISECONDS } from './hubEvent.js'
3
+ import { Tunnel as QuickTunnel } from 'cloudflared'
4
+
5
+ export async function localhostTunnel(port){
6
+ function notifyUser(){
7
+ console.log(`${new Date()}: Direct hub only available within local LAN. Please restart CLI to try again.`)
8
+ }
9
+ const qt = QuickTunnel.quick('127.0.0.1:'+port)
10
+ qt.on('Disconnected', ()=>{notifyUser()})
11
+ let url = await new Promise((resolve)=>{
12
+ let tunnelURL = ''
13
+ qt.once('url', (qtUrl)=>{tunnelURL=qtUrl})
14
+ qt.once('connected', (conn)=>{resolve(tunnelURL)})
15
+ qt.once('exit', (code)=>{resolve('')})
16
+ qt.once('error', (err)=>{resolve('')})
17
+ })
18
+ if (!url) {
19
+ notifyUser()
20
+ }
21
+ return url
22
+ }
23
+
24
+ export function directHubEvent(serverSocket, argv){
25
+ serverSocket.on('connection', (clientSocket) => {
26
+ if (process.env.NODE_ENV !== 'production') {
27
+ console.log('BizA Client Socket', clientSocket.id)
28
+ clientSocket.on('disconnect', (reason)=>{console.log(`Socket ${clientSocket.id} disconnected. Reason : ${reason}`)})
29
+ }
30
+ clientSocket.on('APISocket', (reqData, resCB)=>{
31
+ const socketResponse = (resp)=>{return {status: resp.status, statusText: resp.statusText, headers: resp.headers, body: resp.data, url: `${argv['hostname']}:${argv['port']+resp.config.url}`}}
32
+ if (argv['subdomain'].localeCompare(reqData.subDomain)==0) {
33
+ const apiAddress = `${argv['secure']==true ? 'https://' : 'http://'}${argv['hostname']}:${argv['port']}`
34
+ axios.request({
35
+ timeout: IDLE_SOCKET_TIMEOUT_MILLISECONDS,
36
+ baseURL: apiAddress,
37
+ url: reqData.path,
38
+ method: reqData.method,
39
+ headers: reqData.headers,
40
+ data: reqData.body,
41
+ // decompress: false, // if we need to interfered default Agent compression
42
+ responseType: reqData.responseType,
43
+ maxContentLength: Infinity,
44
+ })
45
+ .then(response=>{
46
+ resCB(null, socketResponse(response))
47
+ })
48
+ .catch(error=>{
49
+ resCB(error, null)
50
+ })
51
+ }
52
+ else {
53
+ resCB({status: 401, statusText: 'bad subdomain', url: `${argv['hostname']}:${argv['port']+reqData.path}`}, null)
54
+ }
55
+ })
56
+ })
57
+ }
package/bin/hub.js CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import yargs from 'yargs';
4
- import { io as ioc } from "socket.io-client";
4
+ import { io as ioClient } from "socket.io-client";
5
5
  import hubEvent from './hubEvent.js'
6
6
  import { createLogger, transports, format } from "winston";
7
+ import { Server as ioServer } from 'socket.io'
8
+ import os from 'node:os'
9
+ import { directHubEvent, localhostTunnel }from './directHubEvent.js'
7
10
 
8
11
  const logger = createLogger({
9
12
  level: 'info',
@@ -27,7 +30,7 @@ process.on('unhandledRejection', (err) => { //debug
27
30
  logger.error('Unhandled Rejection:', err)
28
31
  });
29
32
 
30
- const port = 3002;
33
+ const defaultPort = 3002;
31
34
  const argv = yargs(process.argv.slice(2))
32
35
  .usage('Usage: $0 [options]')
33
36
  .options('s', {
@@ -39,34 +42,46 @@ const argv = yargs(process.argv.slice(2))
39
42
  })
40
43
  .options('sub', {
41
44
  alias: 'subdomain',
42
- describe: '(Required) Public URL the tunnel server is forwarding to us',
45
+ describe: 'Public URL the tunnel server is forwarding to us',
43
46
  type: 'string',
44
47
  demandOption: true
45
48
  })
46
49
  .options('h', {
47
50
  alias: 'hostname',
48
51
  default: '127.0.0.1',
49
- describe: '(Required) Address of local server for forwarding over socket-tunnel',
52
+ describe: 'Address of API server for forwarding over socket-tunnel. Please emit "HTTP" or "HTTPS" from address',
50
53
  type: 'string',
51
54
  demandOption: true
52
55
  })
53
- .options('d', {
54
- alias: 'dbindex',
55
- default: 2,
56
- describe: '(Required) Biz-A Database Index (Callback Feature)',
57
- type: 'number',
58
- demandOption: false
59
- })
60
56
  .options('p', {
61
57
  alias: 'port',
62
58
  default: 212,
63
- describe: 'Port of local server for forwarding over socket-tunnel',
59
+ describe: 'Port of API server for forwarding over socket-tunnel',
60
+ type: 'number',
61
+ demandOption: false
62
+ })
63
+ .options('secure', {
64
+ default: false,
65
+ describe: 'Is API server using ssl (HTTPS) ?',
66
+ type: 'boolean',
67
+ demandOption: false
68
+ })
69
+ .options('publish', {
70
+ default: true,
71
+ describe: 'Will the CLI be published to the internet??',
72
+ type: 'boolean',
73
+ demandOption: false
74
+ })
75
+ .options('d', {
76
+ alias: 'dbindex',
77
+ default: 2,
78
+ describe: 'Biz-A Database Index (Callback Feature)',
64
79
  type: 'number',
65
80
  demandOption: false
66
81
  })
67
82
  .options('sp', {
68
83
  alias: 'serverport',
69
- default: port,
84
+ default: defaultPort,
70
85
  describe: 'Express Port (Callback Feature)',
71
86
  type: 'number',
72
87
  demandOption: false
@@ -89,12 +104,15 @@ if (!argv['server'] || !argv['subdomain'] || !argv['port']) {
89
104
  }
90
105
  }
91
106
 
92
- //
93
107
  import express from 'express';
94
108
  import compression from 'compression';
95
109
  import cors from 'cors';
96
110
  const app = express();
97
111
  import { runCliScript } from '../callbackController.js'
112
+ import fs from "fs"
113
+ import http from 'http'
114
+ import https from 'https'
115
+ import path from 'node:path'
98
116
 
99
117
  app.use(compression());
100
118
  app.use(cors());
@@ -102,17 +120,42 @@ app.use(express.json({ limit: '100mb' }));
102
120
 
103
121
  app.set('args', argv);
104
122
 
105
-
106
123
  app.use('/cb', runCliScript);
107
124
 
108
- app.listen((argv.serverport || port), () => {
109
- console.log(`Biz-A is listening at ${process.env.HOST || 'http://localhost'}:${argv.serverport || port} `);
125
+
126
+ // create HTTP(s) Server
127
+ const keyFile = path.join(import.meta.dirname, "../cert/key.pem")
128
+ const certFile = path.join(import.meta.dirname, "../cert/cert.pem")
129
+ const rootFile = path.join(import.meta.dirname, "../cert/root.pem")
130
+ const isHttps = (fs.existsSync(keyFile) && fs.existsSync(certFile) && fs.existsSync(rootFile))
131
+ const getProtocol = ()=>(isHttps ? 'Https://' : 'Http://')
132
+ let server = isHttps ? https.createServer({key: fs.readFileSync(keyFile), cert: fs.readFileSync(certFile), ca: fs.readFileSync(rootFile),}, app) : http.createServer(app)
133
+
134
+
135
+ // publish CLI with tunnel
136
+ argv.publicUrl = (argv.publish==true) ? await localhostTunnel(argv.serverport) : ''
137
+
138
+ // prepare CLI Address
139
+ argv.cliAddress = ()=>{
140
+ const ip = Object.values(os.networkInterfaces()).flat().reduce((ip, {family, address, internal})=> ip || !internal && family==='IPv4' && address, undefined)
141
+ return {ip, port: argv.serverport, address: `${ip}:${argv.serverport}`, publicUrl: argv.publicUrl}
142
+ }
143
+ const cliAddress = argv.cliAddress()
144
+ server.listen(argv.serverport, () => {
145
+ console.log(`${new Date()}: CLI is listening at ${getProtocol() + (process.env.HOST || cliAddress.ip || 'localhost')}:${cliAddress.port} `);
110
146
  });
111
- //
112
147
 
113
- let socket = ioc(argv['server']);
114
- hubEvent(socket, argv);
115
148
 
116
- app.set('socket', socket)
149
+ // BizA CLI as socket client to BizA SERVER
150
+ await hubEvent(ioClient(argv['server'], {query: {cliAddress: cliAddress.address }}), argv);
151
+ console.log(`${new Date()}: CLI connected to BizA Server using sub domain "${argv['subdomain']}"`)
152
+
153
+
154
+ // BizA CLI as socket server for BizA CLIENT
155
+ const serverCORSOrigin = ['https://biz-a.id', 'https://test.biz-a.id', /\.biz-a\.id$/].concat((process.env.NODE_ENV === 'production') ? [] : [`http://${cliAddress.ip}:4200`, 'http://localhost:4200'])
156
+ // console.log('Allowed Origins', serverCORSOrigin)
157
+ directHubEvent(new ioServer(server, {cors: {origin: serverCORSOrigin}}), argv)
158
+ console.log(`${new Date()}: CLI is listening at "${cliAddress.publicUrl ? cliAddress.publicUrl : cliAddress.address}"`)
159
+
117
160
 
118
161
  export { app }
package/bin/hubEvent.js CHANGED
@@ -1,62 +1,112 @@
1
1
  import axios from 'axios';
2
2
  import net from 'node:net';
3
+ import tls from 'node:tls';
3
4
  import { createRequire } from "module";
4
5
  const require = createRequire(import.meta.url);
5
6
  const ss = require('socket.io-stream'); //SCY: Temporary, next will be replaced with import
7
+ import { Transform } from 'node:stream'
8
+ // import { pipeline } from 'node:stream'
6
9
 
7
- const IDLE_SOCKET_TIMEOUT_MILLISECONDS = 1000 * 30;
10
+ export const IDLE_SOCKET_TIMEOUT_MILLISECONDS = 1000 * 30;
11
+
12
+ export const socketAgent = isUsingHttps=>(isUsingHttps==true) ? tls : net
8
13
 
9
14
  export default async (socket, argv) => new Promise((resolve, reject) => {
15
+
10
16
  const connectCb = () => {
11
- console.log(new Date() + ': connected to socket server');
12
- console.log(new Date() + ': requesting subdomain ' + argv['subdomain'] + ' via ' + argv['server']);
17
+ // console.log(new Date() + ': connected to BizA Server');
18
+ // console.log(new Date() + ': requesting subdomain ' + argv['subdomain'] + ' via ' + argv['server']);
13
19
 
14
20
  socket.emit('createTunnel', argv['subdomain'], (err) => {
15
21
  if (err) {
16
22
  console.log(new Date() + ': [error] ' + err);
17
23
  reject(err);
18
24
  } else {
19
- console.log(new Date() + ': registered with server successfully');
25
+ // console.log(new Date() + ': registered with server successfully');
20
26
 
21
- // clean and concat requested url
22
- let url;
23
- // let subdomain = argv['subdomain'].toString();
24
- let server = argv['server'].toString();
27
+ // // clean and concat requested url
28
+ // let url;
29
+ // // let subdomain = argv['subdomain'].toString();
30
+ // let server = argv['server'].toString();
25
31
 
26
- url = server;
32
+ // url = server;
27
33
 
28
- // resolve promise with requested URL
29
- resolve(url);
34
+ // // resolve promise with requested URL
35
+ // resolve(url);
36
+
37
+ resolve(argv['server'].toString());
30
38
  }
31
39
  });
32
40
  }
41
+
33
42
  const incomingHubCb = (clientId) => {
34
- console.log(clientId, 'incoming clientId')
35
- let client = net.connect(argv['port'], argv['hostname']);
43
+ // console.log(clientId, 'incoming clientId')
44
+
45
+ let addCLIAddressAsResponseHeader = new Transform({
46
+ transform(chunk, encoding, next){
47
+ const apiResponse = chunk.toString().toLowerCase()
48
+ const cliAddress = argv.cliAddress()
49
+ if ( (apiResponse.indexOf('200 ok') > -1) && (apiResponse.indexOf('server: datasnaphttpservice') > -1) && (cliAddress.publicUrl || cliAddress.address)) {
50
+ // don't use string to insert additional headers, chunk can have mixed content of string and binary data
51
+ const response = Buffer.from(chunk)
52
+ const delimiter = '\r\n\r\n'
53
+ const delimiterPos = response.indexOf(delimiter)
54
+ const header = Buffer.concat([
55
+ Buffer.copyBytesFrom(response, 0, delimiterPos),
56
+ Buffer.from(
57
+ '\r\n' +
58
+ 'Access-Control-Expose-Headers: biza-cli-address\r\n' +
59
+ `biza-cli-address: ${cliAddress.publicUrl ? cliAddress.publicUrl : cliAddress.address}\r\n` +
60
+ '\r\n'
61
+ )
62
+ ])
63
+ const body = (response.length>delimiterPos+delimiter.length) ? Buffer.copyBytesFrom(response, delimiterPos+4) : Buffer.from('')
64
+ this.push(body.length>0 ? Buffer.concat([header, body]) : header)
65
+ }
66
+ else {
67
+ this.push(chunk)
68
+ }
69
+ next()
70
+ }
71
+ })
36
72
 
73
+ // let client = net.connect(argv['port'], argv['hostname']);
74
+ let client = socketAgent(argv['secure']).connect(argv['port'], argv['hostname']);
37
75
  client.on('connect', () => {
38
- console.log(`client connected to ${argv['hostname']}:${argv['port']}`)
76
+ // console.log(`client connected to ${argv['hostname']}:${argv['port']}`)
39
77
  let s = ss.createStream();
40
- s.pipe(client).pipe(s);
78
+
79
+ // s.pipe(client).pipe(s);
80
+ s.pipe(client).pipe(addCLIAddressAsResponseHeader).pipe(s)
81
+
82
+ // use pipeline for better performance, back pressure, memory management, error handling and clean up
83
+ // pipeline(s, client, addCLIAddressAsResponseHeader, s, (err)=>{if (err) {console.error(err)}}) // Not work well with heroku
41
84
 
42
85
  s.on('end', () => {
43
- client.destroy();
44
- });
86
+ client.destroy()
87
+ })
88
+
89
+ socket.once(clientId, ()=>{ // hub server shall notify us to end pipeline as soon as possible
90
+ client.end()
91
+ })
92
+
45
93
  ss(socket).emit(clientId, s);
46
94
  })
47
95
 
48
96
  client.setTimeout(IDLE_SOCKET_TIMEOUT_MILLISECONDS);
49
- client.on('timeout', () => {
50
- client.end();
97
+ client.on('timeout', () => { // in case client not notify to end pipeline, then inactivity timeout will end it
98
+ client.end()
51
99
  });
52
100
 
53
- client.on('error', () => {
101
+ client.on('error', (err) => {
54
102
  // handle connection refusal (create a stream and immediately close it)
103
+ console.error('API Error : ', err)
55
104
  let s = ss.createStream();
56
105
  ss(socket).emit(clientId, s);
57
106
  s.end();
58
107
  });
59
108
  }
109
+
60
110
  const cliReqCb = async (data, callback) => {
61
111
  const { path, method, ...remainData } = data;
62
112
 
@@ -67,6 +117,7 @@ export default async (socket, argv) => new Promise((resolve, reject) => {
67
117
  })
68
118
  callback(result.data);
69
119
  }
120
+
70
121
  socket.on('connect', connectCb);
71
122
  socket.on('incomingClient', incomingHubCb)
72
123
  socket.on('cli-req', cliReqCb);
package/mailController.js CHANGED
@@ -6,7 +6,7 @@ function createEmailTransport(req, config) {
6
6
  );
7
7
  }
8
8
 
9
- export async function sendMailWatcher(req) {
9
+ export async function sendMailCliScript(req) {
10
10
  try {
11
11
  const transporter = createEmailTransport(req, req.body.config);
12
12
  const sendResult = await transporter.sendMail(req.body.mailOptions);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "biz-a-cli",
3
3
  "nameDev": "biz-a-cli-dev",
4
- "version": "2.3.43",
4
+ "version": "2.3.53",
5
5
  "versionDev": "0.0.30",
6
6
  "description": "",
7
7
  "main": "bin/index.js",
@@ -28,7 +28,8 @@
28
28
  "biza": "bin/app.js"
29
29
  },
30
30
  "dependencies": {
31
- "axios": "^1.6.8",
31
+ "axios": "^1.7.8",
32
+ "cloudflared": "^0.6.0",
32
33
  "compression": "^1.7.5",
33
34
  "cors": "^2.8.5",
34
35
  "dayjs": "^1.11.10",
@@ -36,6 +37,7 @@
36
37
  "mongodb": "^6.5.0",
37
38
  "net": "^1.0.2",
38
39
  "nodemailer": "^6.9.12",
40
+ "socket.io": "^4.7.5",
39
41
  "socket.io-client": "^4.7.5",
40
42
  "socket.io-stream": "^0.9.1",
41
43
  "tar": "^7.4.0",
@@ -45,8 +47,7 @@
45
47
  "yargs": "^17.7.2"
46
48
  },
47
49
  "devDependencies": {
48
- "jest": "^29.7.0",
49
- "socket.io": "^4.7.5"
50
+ "jest": "^29.7.0"
50
51
  },
51
52
  "jest": {
52
53
  "transform": {},
package/readme.md CHANGED
@@ -61,4 +61,25 @@
61
61
 
62
62
 
63
63
  > [!CAUTION]
64
- > **The *uploadBizA*, *uploadApp*, *deleteApp* commands are deprecated and will be removed in the future.**
64
+ > **The *uploadBizA*, *uploadApp*, *deleteApp* commands are deprecated and will be removed in the future.**
65
+
66
+
67
+ ## III. Hub
68
+
69
+ ### a. Using HTTP
70
+ hub --server [BizA Hub Server] --sub [subdomain] --hostname [ip_API] --port [port_API]
71
+
72
+ Example :
73
+ hub --server https://biz-a.herokuapp.com --sub imamatek --hostname localhost --port 212
74
+
75
+ ### b. Using HTTPs (SSL)
76
+ hub --server [BizA Hub Server] --sub [subdomain] --hostname [ip_API] --port [port_API] --secure
77
+
78
+ Example :
79
+ hub --server https://biz-a.herokuapp.com --sub imamatek --hostname localhost --port 212 --secure
80
+
81
+ ### c. Publish Hub
82
+ hub --server [BizA Hub Server] --sub [subdomain] --hostname [ip_API] --port [port_API] --publish [true]
83
+
84
+ Example :
85
+ hub --server https://biz-a.herokuapp.com --sub imamatek --hostname localhost --port 212 --publish=false
@@ -221,13 +221,7 @@ export function getInputData(config, trigger) {
221
221
  dbindex: config.dbindex,
222
222
  finaDbIndex: config.finaDbIndex,
223
223
  subdomain: config.subdomain,
224
- smtp: {
225
- user: config.smtp.auth.user,
226
- pass: config.smtp.auth.pass,
227
- host: config.smtp.host,
228
- port: config.smtp.port,
229
- secure: config.smtp.secure
230
- }
224
+ smtp: config.smtp
231
225
  },
232
226
  body: trigger.data
233
227
  }
@@ -112,21 +112,12 @@ describe('data test', () => {
112
112
  arguments: {
113
113
  hostname: 'localhost',
114
114
  port: 212,
115
- dbindex: 2,
116
- finaDbIndex: 1,
117
- subdomain: 'abc',
118
- smtp: {
119
- user: 'abc',
120
- pass: '123',
121
- host: 'mail.imamatek.com',
122
- port: 465,
123
- secure: true
124
- }
115
+ dbindex: config.dbindex,
116
+ finaDbIndex: config.finaDbIndex,
117
+ subdomain: config.subdomain,
118
+ smtp: config.smtp
125
119
  },
126
- body: {
127
- _id: 1,
128
- name: 'New Watcher 1'
129
- }
120
+ body: trigger.data
130
121
  });
131
122
  });
132
123
 
@@ -11,12 +11,12 @@ jest.unstable_mockModule('nodemailer', () => ({
11
11
  createTransport: (data) => mockCreateTransport(data)
12
12
  }))
13
13
 
14
- const { sendMailWatcher } = await import('../mailController.js');
14
+ const { sendMailCliScript } = await import('../mailController.js');
15
15
 
16
16
  describe('Mail Controller', () => {
17
17
  let req;
18
18
 
19
- test('transporter.sendmailWatcher is called', async () => {
19
+ test('transporter.sendMailCliScript is called', async () => {
20
20
  req = {
21
21
  body: {
22
22
  companyname: 'abc',
@@ -27,12 +27,12 @@ describe('Mail Controller', () => {
27
27
  }
28
28
 
29
29
  mockSendMail.mockResolvedValue('OK');
30
- expect(await sendMailWatcher(req)).toEqual('OK');
30
+ expect(await sendMailCliScript(req)).toEqual('OK');
31
31
  expect(mockSendMail).toBeCalledTimes(1);
32
32
  expect(mockCreateTransport).toHaveBeenCalledWith({ smtp: 'test' });
33
33
 
34
34
  mockSendMail.mockRejectedValue({ message: 'error' });
35
- expect(await sendMailWatcher(req)).toEqual('error');
35
+ expect(await sendMailCliScript(req)).toEqual('error');
36
36
 
37
37
  expect(mockSendMail).toBeCalledTimes(2);
38
38
  })