@n42/cli 0.1.21
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/LICENSE.md +21 -0
- package/README.md +92 -0
- package/package.json +24 -0
- package/src/auth.js +123 -0
- package/src/browser.js +10 -0
- package/src/cli.js +70 -0
- package/src/config.js +46 -0
- package/src/discover.js +151 -0
- package/src/errors.js +27 -0
- package/src/signin.js +58 -0
- package/src/user.js +64 -0
- package/src/utils.js +134 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2026, Node42. <code@node42.dev>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
+
so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Node42 CLI (n42)
|
|
2
|
+
|
|
3
|
+
Command-line interface for **eDelivery discovery, diagnostics, and
|
|
4
|
+
validation**, with support for the Peppol network.
|
|
5
|
+
|
|
6
|
+
The Node42 CLI is designed for **system integrators, service providers,
|
|
7
|
+
and operators** who need fast, repeatable insight into eDelivery
|
|
8
|
+
routing, SMP resolution, and Access Point behavior.
|
|
9
|
+
|
|
10
|
+
------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Peppol eDelivery path discovery
|
|
15
|
+
- SMP and Access Point resolution diagnostics
|
|
16
|
+
- Supported document type detection
|
|
17
|
+
- PlantUML and SVG visualizations
|
|
18
|
+
- Authenticated API access
|
|
19
|
+
- Deterministic, script-friendly output
|
|
20
|
+
- No browser automation or UI side effects
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### Requirements
|
|
25
|
+
|
|
26
|
+
- Node.js **18+** (Node 20 recommended)
|
|
27
|
+
- npm
|
|
28
|
+
|
|
29
|
+
### Install globally
|
|
30
|
+
|
|
31
|
+
``` bash
|
|
32
|
+
npm install -g @n42/cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Verify installation:
|
|
36
|
+
|
|
37
|
+
``` bash
|
|
38
|
+
n42 --version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
## Authentication
|
|
44
|
+
|
|
45
|
+
Authenticate once using your Node42 account credentials.\
|
|
46
|
+
Tokens are stored locally under `~/.node42/`.
|
|
47
|
+
|
|
48
|
+
``` bash
|
|
49
|
+
n42 signin
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Check authentication status:
|
|
53
|
+
|
|
54
|
+
``` bash
|
|
55
|
+
n42 me
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
## Peppol Discovery
|
|
61
|
+
|
|
62
|
+
### Basic discovery
|
|
63
|
+
|
|
64
|
+
``` bash
|
|
65
|
+
n42 discover peppol <environment> <participantId>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
## Artifacts
|
|
71
|
+
|
|
72
|
+
Artifacts are stored under:
|
|
73
|
+
|
|
74
|
+
~/.node42/artifacts/
|
|
75
|
+
|
|
76
|
+
------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
## Error Handling
|
|
79
|
+
|
|
80
|
+
Errors are printed with a clickable reference link:
|
|
81
|
+
|
|
82
|
+
https://www.node42.dev/errors?code=XXXX
|
|
83
|
+
|
|
84
|
+
## Security
|
|
85
|
+
|
|
86
|
+
- TLS verification enabled by default
|
|
87
|
+
- Explicit `--insecure` flag for testing only
|
|
88
|
+
- Tokens stored locally, never logged
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT License
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@n42/cli",
|
|
3
|
+
"version": "0.1.21",
|
|
4
|
+
"description": "Node42 CLI – Command-line interface for eDelivery path discovery, diagnostics, and tooling",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node42"
|
|
7
|
+
],
|
|
8
|
+
"homepage": "https://github.com/node42-dev/node42-cli",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/node42-dev/node42-cli.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Node42 @n42",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"bin": {
|
|
17
|
+
"n42": "src/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^11.1.0",
|
|
21
|
+
"inquirer": "^8.2.7",
|
|
22
|
+
"open": "^11.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { NODE42_DIR, TOKENS_FILE, API_URL, EP_REFRESH } = require("./config");
|
|
3
|
+
const { handleError } = require("./errors");
|
|
4
|
+
const { updateUserInfo, updateUserUsage } = require("./user");
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function loadAuth() {
|
|
8
|
+
if (!fs.existsSync(TOKENS_FILE)) {
|
|
9
|
+
console.error("Not logged in. Run: n42 signin");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
return JSON.parse(fs.readFileSync(TOKENS_FILE, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function checkAuth() {
|
|
16
|
+
if (!fs.existsSync(TOKENS_FILE)) {
|
|
17
|
+
handleError({ code: "N42E-9033", message: "Token missing..."})
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const res = await fetchWithAuth(`${API_URL}/users/me`, {
|
|
22
|
+
method: "GET",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json"
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!res) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const err = await res.json();
|
|
34
|
+
await handleError(err);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const auth = await res.json();
|
|
39
|
+
if (auth) {
|
|
40
|
+
updateUserInfo({
|
|
41
|
+
userName: auth.userName,
|
|
42
|
+
userMail: auth.userMail,
|
|
43
|
+
role: auth.role,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
updateUserUsage({ serviceUsage: auth.serviceUsage });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function refreshSession() {
|
|
54
|
+
const { refreshToken } = loadAuth();
|
|
55
|
+
if (!refreshToken) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const payload = {
|
|
60
|
+
refreshToken,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const res = await fetch(`${API_URL}/${EP_REFRESH}`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json'
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(payload)
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const data = await res.json();
|
|
72
|
+
//console.log(data);
|
|
73
|
+
|
|
74
|
+
if (!res.ok || data.__type) {
|
|
75
|
+
//console.log(data);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (data) {
|
|
80
|
+
fs.mkdirSync(NODE42_DIR, { recursive: true });
|
|
81
|
+
fs.writeFileSync(
|
|
82
|
+
TOKENS_FILE,
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
accessToken: data.accessToken,
|
|
85
|
+
refreshToken: data.refreshToken,
|
|
86
|
+
idToken: data.idToken
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
//console.log("Token refreshed");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function fetchWithAuth(url, options = {}) {
|
|
96
|
+
const { accessToken } = loadAuth();
|
|
97
|
+
if (!accessToken) { // N42E-9032
|
|
98
|
+
//console.log("Token missing...");
|
|
99
|
+
|
|
100
|
+
handleError({ code: "N42E-9032" });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const res = await fetch(url, {
|
|
105
|
+
...options,
|
|
106
|
+
headers: {
|
|
107
|
+
...(options.headers || {}),
|
|
108
|
+
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (res.status !== 401) {
|
|
113
|
+
return res;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const refreshed = await refreshSession();
|
|
117
|
+
if (!refreshed) { // N42E-9033
|
|
118
|
+
//console.log("Token expired...");
|
|
119
|
+
return res;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { loadAuth, checkAuth, fetchWithAuth };
|
package/src/browser.js
ADDED
package/src/cli.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { Command } = require("commander");
|
|
3
|
+
const { signin } = require("./signin");
|
|
4
|
+
const { checkAuth } = require("./auth");
|
|
5
|
+
const { getUserInfo } = require("./user");
|
|
6
|
+
const { runDiscovery } = require("./discover");
|
|
7
|
+
const { clearScreen, startSpinner, validateEnv, validateId} = require("./utils");
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
const pkg = require("../package.json");
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name("n42")
|
|
14
|
+
.description("Command-line interface for eDelivery path discovery and diagnostics")
|
|
15
|
+
.version(pkg.version);
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command("signin")
|
|
19
|
+
.description("Authenticate using username and password and store tokens locally")
|
|
20
|
+
.action(signin);
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command("me")
|
|
24
|
+
.description("returns identity and billing status for the authenticated user.")
|
|
25
|
+
.action(() => {
|
|
26
|
+
const stopSpinner = startSpinner();
|
|
27
|
+
|
|
28
|
+
checkAuth();
|
|
29
|
+
const user = getUserInfo();
|
|
30
|
+
console.log(
|
|
31
|
+
`Authenticated as ${user.userName} <${user.userMail}> (${user.role})`
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
stopSpinner();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const discover = program
|
|
38
|
+
.command("discover")
|
|
39
|
+
.description("Discovery and diagnostic tooling for eDelivery paths");
|
|
40
|
+
|
|
41
|
+
discover
|
|
42
|
+
.command("peppol <environment> <participantId>")
|
|
43
|
+
.description("Resolve the Peppol eDelivery message path")
|
|
44
|
+
.option("--output <type>", "Result type (json | plantuml)", "json")
|
|
45
|
+
.option("--format <format>", "When output=plantuml (svg | text)", "svg")
|
|
46
|
+
.option("--force-https", "Force HTTPS endpoints", true)
|
|
47
|
+
.option("--insecure", "Disable TLS certificate validation", false)
|
|
48
|
+
.option("--fetch-business-card", "Fetch Peppol business card", false)
|
|
49
|
+
.option("--reverse-lookup", "Enable reverse lookup", false)
|
|
50
|
+
.option("--probe-endpoints", "Probe resolved endpoints", false)
|
|
51
|
+
.action((environment, participantId, options) => {
|
|
52
|
+
|
|
53
|
+
clearScreen(`Node42 CLI v${pkg.version}`);
|
|
54
|
+
|
|
55
|
+
try { validateEnv(environment); }
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.error(e.message);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try { validateId("participant", participantId); }
|
|
62
|
+
catch (e) {
|
|
63
|
+
console.error(e.message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
runDiscovery(environment.toUpperCase(), participantId, options);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
program.parse(process.argv);
|
package/src/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
const config = {
|
|
6
|
+
APP_NAME: "n42",
|
|
7
|
+
API_URL: "https://api.node42.dev",
|
|
8
|
+
WWW_URL: "https://www.node42.dev",
|
|
9
|
+
API_TIMEOUT_MS: 30000,
|
|
10
|
+
|
|
11
|
+
NODE42_DIR: path.join(os.homedir(), ".node42"),
|
|
12
|
+
ARTEFACTS_DIR: null,
|
|
13
|
+
USAGE_FILE: null, // filled below
|
|
14
|
+
USER_FILE: null, // filled below
|
|
15
|
+
TOKENS_FILE: null, // filled below
|
|
16
|
+
CONFIG_FILE: null, // filled below
|
|
17
|
+
|
|
18
|
+
DEFAULT_OUTPUT: "json",
|
|
19
|
+
DEFAULT_FORMAT: "svg",
|
|
20
|
+
|
|
21
|
+
EP_SIGNIN: "auth/signin",
|
|
22
|
+
EP_REFRESH: "auth/refresh",
|
|
23
|
+
EP_DISCOVER: "discover/peppol"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
config.ARTEFACTS_DIR = path.join(config.NODE42_DIR, "artefacts", "discovery");
|
|
27
|
+
config.USER_FILE = path.join(config.NODE42_DIR, "user.json");
|
|
28
|
+
config.USAGE_FILE = path.join(config.NODE42_DIR, "usage.json");
|
|
29
|
+
config.TOKENS_FILE = path.join(config.NODE42_DIR, "tokens.json");
|
|
30
|
+
config.CONFIG_FILE = path.join(config.NODE42_DIR, "config.json");
|
|
31
|
+
|
|
32
|
+
function createAppDirs() {
|
|
33
|
+
fs.mkdirSync(config.NODE42_DIR, { recursive: true });
|
|
34
|
+
fs.mkdirSync(config.ARTEFACTS_DIR, { recursive: true });
|
|
35
|
+
|
|
36
|
+
//if (!fs.existsSync(config.CONFIG_FILE)) {
|
|
37
|
+
fs.writeFileSync(
|
|
38
|
+
config.CONFIG_FILE,
|
|
39
|
+
JSON.stringify(config, null, 2)
|
|
40
|
+
);
|
|
41
|
+
//}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
createAppDirs();
|
|
45
|
+
|
|
46
|
+
module.exports = config;
|
package/src/discover.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const pkg = require("../package.json");
|
|
4
|
+
|
|
5
|
+
const { fetchWithAuth } = require("./auth");
|
|
6
|
+
const { API_URL, EP_DISCOVER, DEFAULT_OUTPUT, DEFAULT_FORMAT, ARTEFACTS_DIR } = require("./config");
|
|
7
|
+
const { getUserUsage, updateUserUsage } = require("./user");
|
|
8
|
+
const { clearScreen, startSpinner, buildDocLabel, promptForDocument } = require("./utils");
|
|
9
|
+
const { handleError } = require("./errors");
|
|
10
|
+
|
|
11
|
+
const DEFAULT_DISCOVERY_INPUT = {
|
|
12
|
+
env: "TEST",
|
|
13
|
+
options: {
|
|
14
|
+
forceHttps: true,
|
|
15
|
+
insecure: false,
|
|
16
|
+
fetchBusinessCard: false,
|
|
17
|
+
reverseLookup: false,
|
|
18
|
+
probeEndpoints: false
|
|
19
|
+
},
|
|
20
|
+
participant: {
|
|
21
|
+
scheme: "iso6523-actorid-upis",
|
|
22
|
+
value: "",
|
|
23
|
+
},
|
|
24
|
+
document: {
|
|
25
|
+
scheme: "peppol-doctype-wildcard",
|
|
26
|
+
value: "",
|
|
27
|
+
},
|
|
28
|
+
overrides: {
|
|
29
|
+
smpUrl: "",
|
|
30
|
+
apUrl: "",
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const discoveryInput = DEFAULT_DISCOVERY_INPUT;
|
|
34
|
+
let docSelected = false;
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async function runDiscovery(environment, participantId, options) {
|
|
38
|
+
const {
|
|
39
|
+
output = DEFAULT_OUTPUT,
|
|
40
|
+
format = DEFAULT_FORMAT,
|
|
41
|
+
forceHttps,
|
|
42
|
+
insecure,
|
|
43
|
+
fetchBusinessCard,
|
|
44
|
+
reverseLookup,
|
|
45
|
+
probeEndpoints
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
discoveryInput.participant.value = participantId;
|
|
49
|
+
|
|
50
|
+
const payload = {
|
|
51
|
+
...discoveryInput,
|
|
52
|
+
env: environment,
|
|
53
|
+
options: {
|
|
54
|
+
output,
|
|
55
|
+
format,
|
|
56
|
+
forceHttps,
|
|
57
|
+
rejectUnauthorized: insecure,
|
|
58
|
+
fetchBusinessCard,
|
|
59
|
+
reverseLookup,
|
|
60
|
+
probeEndpoints
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
clearScreen(`Node42 CLI v${pkg.version}`);
|
|
65
|
+
const stopSpinner = startSpinner();
|
|
66
|
+
|
|
67
|
+
const url = `${API_URL}/${EP_DISCOVER}?output=${output}&format=${format}`;
|
|
68
|
+
const res = await fetchWithAuth(url, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json"
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(payload)
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const err = await res.json();
|
|
78
|
+
stopSpinner();
|
|
79
|
+
|
|
80
|
+
if (err.code) {
|
|
81
|
+
await handleError(err);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (output === "plantuml" && format === "svg") {
|
|
88
|
+
const svg = await res.text();
|
|
89
|
+
stopSpinner();
|
|
90
|
+
|
|
91
|
+
if (!svg || svg.trim().length === 0) {
|
|
92
|
+
await handleError({ code: "6123" });
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const refId = res.headers.get("X-Node42-RefId");
|
|
97
|
+
const serviceUsage = res.headers.get("X-Node42-ServiceUsage");
|
|
98
|
+
const rateLimit = res.headers.get("X-Node42-RateLimit");
|
|
99
|
+
|
|
100
|
+
const userUsage = getUserUsage();
|
|
101
|
+
const currentMonth = new Date().toISOString().slice(0, 7);
|
|
102
|
+
userUsage.serviceUsage.discovery[currentMonth] = serviceUsage;
|
|
103
|
+
|
|
104
|
+
updateUserUsage(userUsage);
|
|
105
|
+
|
|
106
|
+
const encodedDocs = res.headers.get("X-Node42-Documents");
|
|
107
|
+
if (encodedDocs && !docSelected) {
|
|
108
|
+
const docs = JSON.parse(Buffer.from(encodedDocs, "base64").toString("utf8"))
|
|
109
|
+
.map(d => ({ ...d, label: buildDocLabel(d) }));
|
|
110
|
+
|
|
111
|
+
if (docs.length) {
|
|
112
|
+
console.log(`Discovery completed`);
|
|
113
|
+
console.log(`Found ${docs.length} supported document type(s)\n`);
|
|
114
|
+
|
|
115
|
+
docSelected = await promptForDocument(docs);
|
|
116
|
+
|
|
117
|
+
if (docSelected.scheme) {
|
|
118
|
+
discoveryInput.document.scheme = docSelected.scheme;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (docSelected.value) {
|
|
122
|
+
discoveryInput.document.value = docSelected.value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
runDiscovery(environment, participantId, options);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const file = path.join(ARTEFACTS_DIR, `${refId}.svg`);
|
|
130
|
+
fs.writeFileSync(file, svg);
|
|
131
|
+
|
|
132
|
+
console.log(`Discovery completed`);
|
|
133
|
+
console.log(`Usage : ${serviceUsage} / ${rateLimit}`);
|
|
134
|
+
console.log(`Artifact : ${file}\n`);
|
|
135
|
+
|
|
136
|
+
stopSpinner();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (output === "plantuml" && output === "text") {
|
|
141
|
+
const text = await res.text();
|
|
142
|
+
console.log(text);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// default: json
|
|
147
|
+
const json = await res.json();
|
|
148
|
+
console.log(JSON.stringify(json, null, 2));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { runDiscovery };
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { openOnce } = require("./browser");
|
|
2
|
+
const { WWW_URL } = require("./config");
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
async function handleError(err) {
|
|
6
|
+
//console.log(err);
|
|
7
|
+
|
|
8
|
+
const code = err.code?.startsWith("N42E-")
|
|
9
|
+
? err.code.slice(5)
|
|
10
|
+
: undefined;
|
|
11
|
+
|
|
12
|
+
const message = err.message;
|
|
13
|
+
|
|
14
|
+
const url = code
|
|
15
|
+
? `${WWW_URL}/errors?code=${code}`
|
|
16
|
+
: `${WWW_URL}/errors`;
|
|
17
|
+
//console.log(url);
|
|
18
|
+
|
|
19
|
+
if (message) {
|
|
20
|
+
console.error(`${err.message}\nSee details: ${url}\n`);
|
|
21
|
+
} else {
|
|
22
|
+
console.error(`See details: ${url}\n`);
|
|
23
|
+
}
|
|
24
|
+
//await openOnce(url);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { handleError };
|
package/src/signin.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
|
|
3
|
+
const { NODE42_DIR, TOKENS_FILE, API_URL, EP_SIGNIN } = require("./config");
|
|
4
|
+
const { checkAuth } = require("./auth");
|
|
5
|
+
const { getUserInfo } = require("./user");
|
|
6
|
+
const { clearScreen, ask, startSpinner } = require("./utils");
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async function signin() {
|
|
10
|
+
clearScreen("Sign in to Node42");
|
|
11
|
+
|
|
12
|
+
const username = await ask("Username: ");
|
|
13
|
+
const password = await ask("Password: ", true);
|
|
14
|
+
|
|
15
|
+
let stopSpinner = startSpinner();
|
|
16
|
+
|
|
17
|
+
const res = await fetch(`${API_URL}/${EP_SIGNIN}`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
body: JSON.stringify({ username, password })
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
stopSpinner();
|
|
25
|
+
|
|
26
|
+
console.error("Login failed: ", res.status);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tokens = await res.json();
|
|
31
|
+
|
|
32
|
+
const { accessToken, refreshToken, idToken } = tokens;
|
|
33
|
+
if (!accessToken || !refreshToken || !idToken) {
|
|
34
|
+
stopSpinner();
|
|
35
|
+
|
|
36
|
+
console.error("Invalid auth response");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fs.mkdirSync(NODE42_DIR, { recursive: true });
|
|
41
|
+
fs.writeFileSync(
|
|
42
|
+
TOKENS_FILE,
|
|
43
|
+
JSON.stringify({ accessToken, refreshToken, idToken }, null, 2)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
stopSpinner();
|
|
47
|
+
stopSpinner = startSpinner();
|
|
48
|
+
|
|
49
|
+
checkAuth();
|
|
50
|
+
const user = getUserInfo();
|
|
51
|
+
console.log(
|
|
52
|
+
`Authenticated as ${user.userName} <${user.userMail}> (${user.role})`
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
stopSpinner();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { signin };
|
package/src/user.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { NODE42_DIR, USER_FILE, USAGE_FILE } = require("./config");
|
|
3
|
+
|
|
4
|
+
function updateUserInfo(user) {
|
|
5
|
+
fs.mkdirSync(NODE42_DIR, { recursive: true });
|
|
6
|
+
|
|
7
|
+
fs.writeFileSync(
|
|
8
|
+
USER_FILE,
|
|
9
|
+
JSON.stringify(user, null, 2)
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getUserInfo() {
|
|
14
|
+
try {
|
|
15
|
+
if (!fs.existsSync(USER_FILE)) {
|
|
16
|
+
return {
|
|
17
|
+
userName: "n/a",
|
|
18
|
+
userMail: "n/a",
|
|
19
|
+
role: "n/a"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return JSON.parse(fs.readFileSync(USER_FILE, "utf8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return {
|
|
25
|
+
userName: "n/a",
|
|
26
|
+
userMail: "n/a",
|
|
27
|
+
role: "n/a"
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function updateUserUsage(usage) {
|
|
33
|
+
fs.mkdirSync(NODE42_DIR, { recursive: true });
|
|
34
|
+
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
USAGE_FILE,
|
|
37
|
+
JSON.stringify(usage, null, 2)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getUserUsage() {
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(USAGE_FILE)) {
|
|
44
|
+
return {
|
|
45
|
+
serviceUsage: {
|
|
46
|
+
discovery: {},
|
|
47
|
+
validation: {},
|
|
48
|
+
transactions: {}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return JSON.parse(fs.readFileSync(USAGE_FILE, "utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
return {
|
|
55
|
+
serviceUsage: {
|
|
56
|
+
discovery: {},
|
|
57
|
+
validation: {},
|
|
58
|
+
transactions: {}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { updateUserInfo, getUserInfo, updateUserUsage, getUserUsage };
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const inquirer = require("inquirer");
|
|
2
|
+
const readline = require("readline");
|
|
3
|
+
|
|
4
|
+
function clearScreen(text) {
|
|
5
|
+
process.stdout.write("\x1Bc");
|
|
6
|
+
if (text) {
|
|
7
|
+
process.stdout.write(text + "\n");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function ask(question, hidden=false) {
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
const rl = readline.createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
output: process.stdout,
|
|
16
|
+
terminal: true
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
process.stdin.on("data", char => {
|
|
20
|
+
char = char + "";
|
|
21
|
+
switch (char) {
|
|
22
|
+
case "\n":
|
|
23
|
+
case "\r":
|
|
24
|
+
case "\u0004":
|
|
25
|
+
process.stdin.pause();
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
if (hidden) {
|
|
29
|
+
process.stdout.clearLine(0);
|
|
30
|
+
process.stdout.cursorTo(0);
|
|
31
|
+
process.stdout.write(question + "*".repeat(rl.line.length));
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
rl.question(question, answer => {
|
|
38
|
+
rl.history = rl.history.slice(1);
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve(answer);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function startSpinner(text = "Working") {
|
|
46
|
+
const frames = ["-", "\\", "|", "/"];
|
|
47
|
+
let i = 0;
|
|
48
|
+
|
|
49
|
+
const timer = setInterval(() => {
|
|
50
|
+
process.stdout.write("\r[" + frames[i++ % frames.length] + "] " + text);
|
|
51
|
+
}, 120);
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
clearInterval(timer);
|
|
55
|
+
process.stdout.write("\r\x1b[K"); // carriage return + clear line
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildDocLabel({ scheme, value }) {
|
|
60
|
+
// 1. Document name (after :: before ##)
|
|
61
|
+
const docMatch = value.match(/::([^#]+)##/);
|
|
62
|
+
const docName = docMatch ? docMatch[1].replace(/([A-Z])/g, " $1").trim() : "Document";
|
|
63
|
+
|
|
64
|
+
// 2. Wildcard
|
|
65
|
+
if (value.endsWith("##*")) {
|
|
66
|
+
return `Any ${docName} (Wildcard)`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. PINT profile
|
|
70
|
+
if (value.includes(":pint:")) {
|
|
71
|
+
const regionMatch = value.match(/@([a-z0-9-]+)/i);
|
|
72
|
+
const region = regionMatch
|
|
73
|
+
? regionMatch[1].replace(/-1$/, "").toUpperCase()
|
|
74
|
+
: "PINT";
|
|
75
|
+
|
|
76
|
+
const prefix = scheme === "peppol-doctype-wildcard" ? "Wildcard" : "PINT";
|
|
77
|
+
return `${docName} (${prefix} ${region})`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. BIS profile
|
|
81
|
+
if (value.includes(":bis:") || value.includes("en16931")) {
|
|
82
|
+
const bisMatch = value.match(/bis:[^:]+:(\d+)/);
|
|
83
|
+
const version = bisMatch ? bisMatch[1] : "3";
|
|
84
|
+
return `${docName} (BIS ${version})`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 4. TRNS profile
|
|
88
|
+
if (value.includes(":trns:")) {
|
|
89
|
+
const trnsMatch = value.match(/:trns:([^:]+):([\d.]+)/);
|
|
90
|
+
const version = trnsMatch ? ` ${trnsMatch[2]}` : "";
|
|
91
|
+
return `${docName} (TRNS${version})`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 5. Fallback
|
|
95
|
+
return docName;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function promptForDocument(docs) {
|
|
99
|
+
const { document } = await inquirer.prompt([
|
|
100
|
+
{
|
|
101
|
+
type: "list",
|
|
102
|
+
name: "document",
|
|
103
|
+
message: "Select document type:",
|
|
104
|
+
choices: docs.map(d => ({
|
|
105
|
+
name: d.label,
|
|
106
|
+
value: d
|
|
107
|
+
}))
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
return document;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateEnv(env) {
|
|
115
|
+
const allowedEnvs = ["TEST", "PROD"];
|
|
116
|
+
if (!allowedEnvs.includes(env.toUpperCase())) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Invalid environment: ${env}\nAllowed values: ${allowedEnvs.join(", ")}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateId(type, id) {
|
|
124
|
+
const value = id.replace(/\s+/g, "");
|
|
125
|
+
|
|
126
|
+
// ISO 6523–safe; participant id like 0000:12345 or 9915:abcde
|
|
127
|
+
if (!/^[0-9]{4}:[a-zA-Z0-9\-\._~]{1,135}$/.test(value)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Invalid ${type}Id: ${id}\nExpected format: 0007:123456789 or 0007:abcd`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { clearScreen, startSpinner, ask, buildDocLabel, promptForDocument, validateEnv, validateId };
|