wldx-admin-cli 0.0.0-stage → 1.1.0

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,197 @@
1
+ // wx-admin pbx: the PBX the commands work on and the access saved for them.
2
+ import {savedSession, sessions} from 'wldx-core/auth';
3
+ import {UsageError} from 'wldx-core/errors';
4
+ import {paint, table} from 'wldx-core/format';
5
+ import {
6
+ API_KEY,
7
+ checkPort,
8
+ loginPbx,
9
+ loginPbxOf,
10
+ normalizeHost,
11
+ pbxClient,
12
+ pbxHostOf,
13
+ readSerial,
14
+ removeAccess,
15
+ resolvePbx,
16
+ saveAccess,
17
+ savedAccess,
18
+ savedPbxs,
19
+ setDefaultPbx,
20
+ } from '../pbx.mjs';
21
+ import {readSecret} from '../secret.mjs';
22
+
23
+ const KINDS = {
24
+ apikey: 'a Company API Key (wsk-v1-…), the access Wildix recommends',
25
+ token: 'a Simple Token (deprecated by Wildix: no new ones can be made)',
26
+ basic: 'a user and password (Basic; the PBX accepts it only with ALLOW_BASIC_AUTH=true)',
27
+ };
28
+
29
+ // The PBX of the x-bees login of the profile, if any (no network).
30
+ function loginTarget(ctx) {
31
+ const email = ctx.profile ?? sessions(ctx.env)[0]?.email;
32
+ const set = email ? savedSession(email, ctx.env) : undefined;
33
+ return set ? loginPbx(set.IdToken) : undefined;
34
+ }
35
+
36
+ export const groups = {
37
+ pbx: {
38
+ summary: 'the PBX to work on: saved access (API key, token, user), the default one, a check',
39
+ commands: {
40
+ add: {
41
+ summary: 'save access to a PBX; the secret is typed hidden or read from stdin, never an argument',
42
+ usage: 'wx-admin pbx add <host> (--api-key | --token | --basic [--user NAME]) [--port N] [--no-check]',
43
+ details: [
44
+ 'host: name.wildixin.com (or just name), another host name, or an IP address.',
45
+ ...Object.entries(KINDS).map(([k, v]) => ` --${k === 'apikey' ? 'api-key' : k} ${v}`),
46
+ 'The access is checked with a read of the PBX before it is kept (--no-check skips it).',
47
+ ].join('\n'),
48
+ options: {
49
+ 'api-key': {type: 'boolean', description: 'a Company API Key (wsk-v1-…)'},
50
+ token: {type: 'boolean', description: 'a Simple Token'},
51
+ basic: {type: 'boolean', description: 'a user and password'},
52
+ user: {type: 'string', placeholder: 'NAME', description: 'the user of --basic (default admin)'},
53
+ port: {type: 'string', placeholder: 'N', description: 'the https port of the PBX (default 443)'},
54
+ 'no-check': {type: 'boolean', description: 'keep the access without reading the PBX first'},
55
+ },
56
+ args: [1, 1],
57
+ async run(ctx) {
58
+ const {values} = ctx;
59
+ const kinds = ['api-key', 'token', 'basic'].filter((k) => values[k]);
60
+ if (kinds.length !== 1) throw new UsageError('Give one of --api-key, --token, --basic', ctx.usage());
61
+ if (values.user !== undefined && !values.basic) throw new UsageError('--user goes with --basic', ctx.usage());
62
+ const host = normalizeHost(ctx.positionals[0]);
63
+ const port = values.port === undefined ? 443 : checkPort(values.port);
64
+ const auth = kinds[0] === 'api-key' ? 'apikey' : kinds[0];
65
+ const secret = (
66
+ await readSecret(
67
+ ctx,
68
+ auth === 'basic'
69
+ ? `Password of ${values.user ?? 'admin'} on ${host}: `
70
+ : `${auth === 'apikey' ? 'API key' : 'Token'} for ${host}: `,
71
+ )
72
+ ).trim();
73
+ if (!secret) throw new UsageError('Nothing was entered: nothing was saved');
74
+ if (auth === 'apikey' && !API_KEY.test(secret))
75
+ throw new UsageError('Not a Company API Key: it is wsk-v1- and 93 letters and digits. Nothing was saved.');
76
+ if (auth === 'token' && /\s/.test(secret)) throw new UsageError('A token has no spaces. Nothing was saved.');
77
+ const access = {
78
+ host,
79
+ port,
80
+ auth,
81
+ ...(auth === 'basic'
82
+ ? {username: values.user ?? 'admin', password: secret}
83
+ : auth === 'apikey'
84
+ ? {key: secret}
85
+ : {token: secret}),
86
+ };
87
+ // Checked with one read of the PBX before it is kept: nothing is saved (and an access saved before stays) when
88
+ // the PBX refuses it. The check uses this new access only, never WILDIX_PBX_API_KEY or a saved one.
89
+ if (!values['no-check']) {
90
+ const client = await pbxClient(ctx, {access});
91
+ await client.call('GET', '/Dictionaries');
92
+ // the serial, so that --pbx can name the PBX by it too (null when this access may not read it)
93
+ access.serial = await readSerial(client);
94
+ }
95
+ saveAccess(access, ctx.env);
96
+ return {host, port, auth, serial: access.serial ?? null, checked: !values['no-check']};
97
+ },
98
+ text: (r) =>
99
+ `Saved access to ${r.host}${r.serial ? ` (serial ${r.serial})` : ''} (${r.auth})${r.checked ? ', checked' : ''}. Use it with --pbx ${r.host}, or make it the default: wx-admin pbx use ${r.host}`,
100
+ },
101
+ list: {
102
+ summary: 'the PBX with saved access and the PBX of the x-bees login; * marks the default one',
103
+ usage: 'wx-admin pbx list',
104
+ options: {},
105
+ args: [0, 0],
106
+ run(ctx) {
107
+ const saved = savedPbxs(ctx.env);
108
+ const login = loginTarget(ctx);
109
+ return {
110
+ pbxs: saved,
111
+ login: login ?? null,
112
+ default: saved.find((p) => p.default)?.host ?? (login ? `login:${login.host}` : null),
113
+ };
114
+ },
115
+ text(r, ctx) {
116
+ const c = paint(ctx);
117
+ const rows = r.pbxs.map((p) => [
118
+ p.default ? '*' : ' ',
119
+ p.host + (p.port !== 443 ? `:${p.port}` : ''),
120
+ p.serial ?? '',
121
+ p.auth,
122
+ ]);
123
+ if (r.login)
124
+ rows.push([
125
+ r.pbxs.some((p) => p.default) ? ' ' : '*',
126
+ r.login.host,
127
+ r.login.serial ?? '',
128
+ c.dim('the x-bees login'),
129
+ ]);
130
+ return rows.length
131
+ ? table(rows)
132
+ : 'No PBX: add one (wx-admin pbx add <host>) or log in (wx-admin login <e-mail>)';
133
+ },
134
+ },
135
+ use: {
136
+ summary: 'make a PBX with saved access the default one; --login goes back to the PBX of the x-bees login',
137
+ usage: 'wx-admin pbx use (<host> | --login)',
138
+ options: {login: {type: 'boolean', description: 'no default PBX: the PBX of the x-bees login'}},
139
+ args: [0, 1],
140
+ complete: 'pbxs',
141
+ run(ctx) {
142
+ if (ctx.values.login) {
143
+ if (ctx.positionals.length) throw new UsageError('Give a host or --login, not both', ctx.usage());
144
+ setDefaultPbx(undefined, ctx.env);
145
+ return {defaultPbx: null};
146
+ }
147
+ if (!ctx.positionals.length) throw new UsageError('Give a host or --login', ctx.usage());
148
+ const host = pbxHostOf(ctx.positionals[0], ctx);
149
+ if (!savedAccess(host, ctx.env))
150
+ throw new UsageError(`No access saved for ${host}: wx-admin pbx add ${host}`);
151
+ setDefaultPbx(host, ctx.env);
152
+ return {defaultPbx: host};
153
+ },
154
+ text: (r) => (r.defaultPbx ? `Default PBX: ${r.defaultPbx}` : 'Default PBX: the one of the x-bees login'),
155
+ },
156
+ remove: {
157
+ summary: 'forget the saved access to a PBX (on this computer only)',
158
+ usage: 'wx-admin pbx remove <host>',
159
+ options: {},
160
+ args: [1, 1],
161
+ complete: 'pbxs',
162
+ run(ctx) {
163
+ const host = pbxHostOf(ctx.positionals[0], ctx);
164
+ if (!removeAccess(host, ctx.env)) throw new UsageError(`No access saved for ${host}`);
165
+ return {removed: host};
166
+ },
167
+ text: (r) => `Removed the access to ${r.removed}`,
168
+ },
169
+ check: {
170
+ readOnly: true,
171
+ summary: 'which PBX a command would use, and whether it accepts the access (one read)',
172
+ usage: 'wx-admin pbx check',
173
+ options: {},
174
+ args: [0, 0],
175
+ async run(ctx) {
176
+ const target = await resolvePbx(ctx);
177
+ const client = await pbxClient(ctx);
178
+ await client.call('GET', '/Dictionaries');
179
+ // A saved access without its serial (added before wx-admin kept it) gets it now: --pbx can then use it.
180
+ const saved =
181
+ target.auth !== 'login' && !target.fromEnvironment ? savedAccess(target.host, ctx.env) : undefined;
182
+ let serial = saved?.serial ?? (target.auth === 'login' ? loginPbxOf(ctx)?.serial : null) ?? null;
183
+ if (saved && !saved.serial) {
184
+ serial = await readSerial(client);
185
+ if (serial) saveAccess({...saved, serial}, ctx.env);
186
+ }
187
+ return {host: target.host, port: target.port, serial, auth: target.auth, by: target.by, accepted: true};
188
+ },
189
+ text: (r) =>
190
+ `${r.host}${r.serial ? ` (serial ${r.serial})` : ''}: the access (${r.auth === 'login' ? 'x-bees login' : r.auth}) is accepted · ${{option: 'chosen with --pbx', default: 'the default PBX', login: 'the PBX of the x-bees login', environment: 'WILDIX_PBX_API_KEY for WILDIX_PBX_HOST'}[r.by]}`,
191
+ },
192
+ },
193
+ },
194
+ };
195
+
196
+ // Used by completion: the hosts with saved access.
197
+ export const pbxHosts = (env) => savedPbxs(env).map((p) => p.host);