@velaro/cli 0.5.0 → 1.2.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.
- package/bin/velaro.js +5 -1
- package/lib/api.js +52 -52
- package/lib/commands/agent.js +50 -50
- package/lib/commands/article.js +120 -12
- package/lib/commands/check.js +163 -163
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/env.js +45 -45
- package/lib/commands/index.js +212 -0
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/kb.js +27 -4
- package/lib/commands/login.js +86 -86
- package/lib/commands/ops.js +173 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/status.js +24 -24
- package/lib/commands/team.js +144 -144
- package/lib/commands/update.js +47 -47
- package/lib/commands/whoami.js +22 -22
- package/lib/config.js +83 -83
- package/lib/oauth.js +135 -135
- package/lib/run.js +16 -16
- package/lib/subscription.js +39 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +2 -2
package/lib/config.js
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
import { homedir } from 'os';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
|
-
|
|
5
|
-
const CONFIG_DIR = join(homedir(), '.velaro');
|
|
6
|
-
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
|
-
|
|
8
|
-
export const ENVS = {
|
|
9
|
-
prod: {
|
|
10
|
-
adminApiBase: 'https://api-admin-us-east.velaro.com',
|
|
11
|
-
messagingApiBase: 'https://velaro-messaging-api-staging.azurewebsites.net',
|
|
12
|
-
},
|
|
13
|
-
staging: {
|
|
14
|
-
adminApiBase: 'https://velaro-admin-staging.azurewebsites.net',
|
|
15
|
-
messagingApiBase: 'https://velaro-messaging-api-staging.azurewebsites.net',
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Legacy single-entry config — migrate transparently on first read.
|
|
20
|
-
function migrate(raw) {
|
|
21
|
-
if (raw.envs) return raw; // already multi-env
|
|
22
|
-
const apiBase = raw.apiBase || '';
|
|
23
|
-
const env = apiBase.includes('staging') ? 'staging' : 'prod';
|
|
24
|
-
return {
|
|
25
|
-
activeEnv: env,
|
|
26
|
-
envs: {
|
|
27
|
-
[env]: {
|
|
28
|
-
adminApiBase: ENVS[env].adminApiBase,
|
|
29
|
-
messagingApiBase: ENVS[env].messagingApiBase,
|
|
30
|
-
velaroToken: raw.velaroToken,
|
|
31
|
-
velaroExpires: raw.velaroExpires,
|
|
32
|
-
entraRefreshToken: raw.entraRefreshToken,
|
|
33
|
-
siteId: raw.siteId,
|
|
34
|
-
userName: raw.userName,
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
_lastUpdateCheck: raw._lastUpdateCheck,
|
|
38
|
-
_latestVersion: raw._latestVersion,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function readConfig() {
|
|
43
|
-
try {
|
|
44
|
-
const raw = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
45
|
-
return migrate(raw);
|
|
46
|
-
} catch {
|
|
47
|
-
return { activeEnv: 'prod', envs: {} };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function writeConfig(data) {
|
|
52
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
53
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function getActiveEnv() {
|
|
57
|
-
return readConfig().activeEnv || 'prod';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function setActiveEnv(env) {
|
|
61
|
-
if (!ENVS[env]) throw new Error(`Unknown environment "${env}". Use: prod, staging`);
|
|
62
|
-
const cfg = readConfig();
|
|
63
|
-
cfg.activeEnv = env;
|
|
64
|
-
writeConfig(cfg);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function getEnvCredentials(env) {
|
|
68
|
-
const cfg = readConfig();
|
|
69
|
-
return cfg.envs?.[env] || null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function setEnvCredentials(env, creds) {
|
|
73
|
-
if (!ENVS[env]) throw new Error(`Unknown environment "${env}". Use: prod, staging`);
|
|
74
|
-
const cfg = readConfig();
|
|
75
|
-
cfg.envs = cfg.envs || {};
|
|
76
|
-
cfg.envs[env] = { ...ENVS[env], ...creds };
|
|
77
|
-
cfg.activeEnv = cfg.activeEnv || env;
|
|
78
|
-
writeConfig(cfg);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function clearConfig() {
|
|
82
|
-
writeConfig({ activeEnv: 'prod', envs: {} });
|
|
83
|
-
}
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), '.velaro');
|
|
6
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
|
+
|
|
8
|
+
export const ENVS = {
|
|
9
|
+
prod: {
|
|
10
|
+
adminApiBase: 'https://api-admin-us-east.velaro.com',
|
|
11
|
+
messagingApiBase: 'https://velaro-messaging-api-staging.azurewebsites.net',
|
|
12
|
+
},
|
|
13
|
+
staging: {
|
|
14
|
+
adminApiBase: 'https://velaro-admin-staging.azurewebsites.net',
|
|
15
|
+
messagingApiBase: 'https://velaro-messaging-api-staging.azurewebsites.net',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Legacy single-entry config — migrate transparently on first read.
|
|
20
|
+
function migrate(raw) {
|
|
21
|
+
if (raw.envs) return raw; // already multi-env
|
|
22
|
+
const apiBase = raw.apiBase || '';
|
|
23
|
+
const env = apiBase.includes('staging') ? 'staging' : 'prod';
|
|
24
|
+
return {
|
|
25
|
+
activeEnv: env,
|
|
26
|
+
envs: {
|
|
27
|
+
[env]: {
|
|
28
|
+
adminApiBase: ENVS[env].adminApiBase,
|
|
29
|
+
messagingApiBase: ENVS[env].messagingApiBase,
|
|
30
|
+
velaroToken: raw.velaroToken,
|
|
31
|
+
velaroExpires: raw.velaroExpires,
|
|
32
|
+
entraRefreshToken: raw.entraRefreshToken,
|
|
33
|
+
siteId: raw.siteId,
|
|
34
|
+
userName: raw.userName,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
_lastUpdateCheck: raw._lastUpdateCheck,
|
|
38
|
+
_latestVersion: raw._latestVersion,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readConfig() {
|
|
43
|
+
try {
|
|
44
|
+
const raw = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
45
|
+
return migrate(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return { activeEnv: 'prod', envs: {} };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeConfig(data) {
|
|
52
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
53
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getActiveEnv() {
|
|
57
|
+
return readConfig().activeEnv || 'prod';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function setActiveEnv(env) {
|
|
61
|
+
if (!ENVS[env]) throw new Error(`Unknown environment "${env}". Use: prod, staging`);
|
|
62
|
+
const cfg = readConfig();
|
|
63
|
+
cfg.activeEnv = env;
|
|
64
|
+
writeConfig(cfg);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getEnvCredentials(env) {
|
|
68
|
+
const cfg = readConfig();
|
|
69
|
+
return cfg.envs?.[env] || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function setEnvCredentials(env, creds) {
|
|
73
|
+
if (!ENVS[env]) throw new Error(`Unknown environment "${env}". Use: prod, staging`);
|
|
74
|
+
const cfg = readConfig();
|
|
75
|
+
cfg.envs = cfg.envs || {};
|
|
76
|
+
cfg.envs[env] = { ...ENVS[env], ...creds };
|
|
77
|
+
cfg.activeEnv = cfg.activeEnv || env;
|
|
78
|
+
writeConfig(cfg);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function clearConfig() {
|
|
82
|
+
writeConfig({ activeEnv: 'prod', envs: {} });
|
|
83
|
+
}
|
package/lib/oauth.js
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OAuth 2.0 device authorization flow (RFC 8628) against the Velaro Entra CIAM tenant.
|
|
3
|
-
*
|
|
4
|
-
* Flow:
|
|
5
|
-
* 1. POST /devicecode → get user_code + verification_uri
|
|
6
|
-
* 2. Show the user the code; poll /token until granted
|
|
7
|
-
* 3. Exchange the Entra access_token for a Velaro JWT via /auth/entra/token
|
|
8
|
-
* 4. Store { velaroToken, velaroExpires, entraRefreshToken } in ~/.velaro/config.json
|
|
9
|
-
*
|
|
10
|
-
* Refresh:
|
|
11
|
-
* When the Velaro JWT is within 5 minutes of expiring, silently use the stored
|
|
12
|
-
* Entra refresh_token to get a new access_token and re-exchange for a new Velaro JWT.
|
|
13
|
-
*
|
|
14
|
-
* One-time setup (run once in the Velaro CIAM tenant):
|
|
15
|
-
* az login --tenant 61de45b3-458d-49a5-913c-501247a6fe4f --allow-no-subscriptions
|
|
16
|
-
* az ad app create --display-name "Velaro CLI" --sign-in-audience AzureADMyOrg \
|
|
17
|
-
* --public-client-redirect-uris "https://login.microsoftonline.com/common/oauth2/nativeclient"
|
|
18
|
-
* az ad app update --id <appId> --set isFallbackPublicClient=true
|
|
19
|
-
* # Grant admin consent in Azure Portal:
|
|
20
|
-
* # api://89bd2fb7-7020-4cfe-a153-1c1ee37903c2/access_as_user (Delegated)
|
|
21
|
-
* # Then set VELARO_CLI_CLIENT_ID=<appId> or update the constant below.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
const TENANT_ID = '61de45b3-458d-49a5-913c-501247a6fe4f';
|
|
25
|
-
// CIAM tenant uses login.velaro.com, not login.microsoftonline.com
|
|
26
|
-
const AUTHORITY = `https://login.velaro.com/${TENANT_ID}`;
|
|
27
|
-
const CLIENT_ID = process.env.VELARO_CLI_CLIENT_ID || 'c0fdce54-e9b4-4427-a021-b2605855ff8b';
|
|
28
|
-
|
|
29
|
-
const SCOPE = [
|
|
30
|
-
'api://89bd2fb7-7020-4cfe-a153-1c1ee37903c2/access_as_user',
|
|
31
|
-
'offline_access',
|
|
32
|
-
'openid',
|
|
33
|
-
'profile',
|
|
34
|
-
'email',
|
|
35
|
-
].join(' ');
|
|
36
|
-
|
|
37
|
-
const DEVICE_CODE_URL = `${AUTHORITY}/oauth2/v2.0/devicecode`;
|
|
38
|
-
const TOKEN_URL = `${AUTHORITY}/oauth2/v2.0/token`;
|
|
39
|
-
const FETCH_TIMEOUT = 10_000;
|
|
40
|
-
|
|
41
|
-
export async function requestDeviceCode() {
|
|
42
|
-
const res = await fetch(DEVICE_CODE_URL, {
|
|
43
|
-
method: 'POST',
|
|
44
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
45
|
-
body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPE }),
|
|
46
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const data = await res.json();
|
|
50
|
-
if (data.error) throw new Error(`Device code request failed: ${data.error_description || data.error}`);
|
|
51
|
-
return data; // { device_code, user_code, verification_uri, expires_in, interval, message }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function pollForToken(deviceCode, intervalSeconds) {
|
|
55
|
-
let interval = intervalSeconds;
|
|
56
|
-
|
|
57
|
-
while (true) {
|
|
58
|
-
const res = await fetch(TOKEN_URL, {
|
|
59
|
-
method: 'POST',
|
|
60
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
61
|
-
body: new URLSearchParams({
|
|
62
|
-
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
63
|
-
client_id: CLIENT_ID,
|
|
64
|
-
device_code: deviceCode,
|
|
65
|
-
}),
|
|
66
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const data = await res.json();
|
|
70
|
-
|
|
71
|
-
if (data.access_token) return data; // { access_token, refresh_token, expires_in }
|
|
72
|
-
|
|
73
|
-
if (data.error === 'authorization_pending') {
|
|
74
|
-
// normal — keep waiting
|
|
75
|
-
} else if (data.error === 'slow_down') {
|
|
76
|
-
interval += 5 + Math.random() * 2; // RFC 8628 §3.5 — add jitter to avoid lockstep
|
|
77
|
-
} else if (data.error === 'expired_token') {
|
|
78
|
-
throw new Error('Login timed out — please run velaro login again.');
|
|
79
|
-
} else if (data.error === 'access_denied') {
|
|
80
|
-
throw new Error('Login cancelled.');
|
|
81
|
-
} else {
|
|
82
|
-
throw new Error(data.error_description || data.error);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
await sleep(interval * 1000); // sleep at end so first attempt fires immediately
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function exchangeForVelaroToken(entraAccessToken, apiBase) {
|
|
90
|
-
const res = await fetch(`${apiBase}/auth/entra/token`, {
|
|
91
|
-
method: 'POST',
|
|
92
|
-
headers: { Authorization: `Bearer ${entraAccessToken}` },
|
|
93
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
if (!res.ok) {
|
|
97
|
-
const body = await res.text().catch(() => res.status.toString());
|
|
98
|
-
throw new Error(`Velaro token exchange failed (${res.status}): ${body}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return res.json(); // { token: { token, expires }, profile: { SiteId, Name, UserName, ... } }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export async function refreshVelaroToken(credentials) {
|
|
105
|
-
const res = await fetch(TOKEN_URL, {
|
|
106
|
-
method: 'POST',
|
|
107
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
108
|
-
body: new URLSearchParams({
|
|
109
|
-
grant_type: 'refresh_token',
|
|
110
|
-
client_id: CLIENT_ID,
|
|
111
|
-
refresh_token: credentials.entraRefreshToken,
|
|
112
|
-
}),
|
|
113
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const data = await res.json();
|
|
117
|
-
if (!data.access_token) {
|
|
118
|
-
throw new Error(`Token refresh failed: ${data.error_description || data.error}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const velaro = await exchangeForVelaroToken(data.access_token, credentials.apiBase);
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
...credentials,
|
|
125
|
-
velaroToken: velaro.token.token,
|
|
126
|
-
velaroExpires: velaro.token.expires,
|
|
127
|
-
entraRefreshToken: data.refresh_token || credentials.entraRefreshToken,
|
|
128
|
-
siteId: velaro.profile?.SiteId ?? credentials.siteId,
|
|
129
|
-
userName: velaro.profile?.Name ?? credentials.userName,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function sleep(ms) {
|
|
134
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 device authorization flow (RFC 8628) against the Velaro Entra CIAM tenant.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. POST /devicecode → get user_code + verification_uri
|
|
6
|
+
* 2. Show the user the code; poll /token until granted
|
|
7
|
+
* 3. Exchange the Entra access_token for a Velaro JWT via /auth/entra/token
|
|
8
|
+
* 4. Store { velaroToken, velaroExpires, entraRefreshToken } in ~/.velaro/config.json
|
|
9
|
+
*
|
|
10
|
+
* Refresh:
|
|
11
|
+
* When the Velaro JWT is within 5 minutes of expiring, silently use the stored
|
|
12
|
+
* Entra refresh_token to get a new access_token and re-exchange for a new Velaro JWT.
|
|
13
|
+
*
|
|
14
|
+
* One-time setup (run once in the Velaro CIAM tenant):
|
|
15
|
+
* az login --tenant 61de45b3-458d-49a5-913c-501247a6fe4f --allow-no-subscriptions
|
|
16
|
+
* az ad app create --display-name "Velaro CLI" --sign-in-audience AzureADMyOrg \
|
|
17
|
+
* --public-client-redirect-uris "https://login.microsoftonline.com/common/oauth2/nativeclient"
|
|
18
|
+
* az ad app update --id <appId> --set isFallbackPublicClient=true
|
|
19
|
+
* # Grant admin consent in Azure Portal:
|
|
20
|
+
* # api://89bd2fb7-7020-4cfe-a153-1c1ee37903c2/access_as_user (Delegated)
|
|
21
|
+
* # Then set VELARO_CLI_CLIENT_ID=<appId> or update the constant below.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const TENANT_ID = '61de45b3-458d-49a5-913c-501247a6fe4f';
|
|
25
|
+
// CIAM tenant uses login.velaro.com, not login.microsoftonline.com
|
|
26
|
+
const AUTHORITY = `https://login.velaro.com/${TENANT_ID}`;
|
|
27
|
+
const CLIENT_ID = process.env.VELARO_CLI_CLIENT_ID || 'c0fdce54-e9b4-4427-a021-b2605855ff8b';
|
|
28
|
+
|
|
29
|
+
const SCOPE = [
|
|
30
|
+
'api://89bd2fb7-7020-4cfe-a153-1c1ee37903c2/access_as_user',
|
|
31
|
+
'offline_access',
|
|
32
|
+
'openid',
|
|
33
|
+
'profile',
|
|
34
|
+
'email',
|
|
35
|
+
].join(' ');
|
|
36
|
+
|
|
37
|
+
const DEVICE_CODE_URL = `${AUTHORITY}/oauth2/v2.0/devicecode`;
|
|
38
|
+
const TOKEN_URL = `${AUTHORITY}/oauth2/v2.0/token`;
|
|
39
|
+
const FETCH_TIMEOUT = 10_000;
|
|
40
|
+
|
|
41
|
+
export async function requestDeviceCode() {
|
|
42
|
+
const res = await fetch(DEVICE_CODE_URL, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
45
|
+
body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPE }),
|
|
46
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
if (data.error) throw new Error(`Device code request failed: ${data.error_description || data.error}`);
|
|
51
|
+
return data; // { device_code, user_code, verification_uri, expires_in, interval, message }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function pollForToken(deviceCode, intervalSeconds) {
|
|
55
|
+
let interval = intervalSeconds;
|
|
56
|
+
|
|
57
|
+
while (true) {
|
|
58
|
+
const res = await fetch(TOKEN_URL, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
61
|
+
body: new URLSearchParams({
|
|
62
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
63
|
+
client_id: CLIENT_ID,
|
|
64
|
+
device_code: deviceCode,
|
|
65
|
+
}),
|
|
66
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
|
|
71
|
+
if (data.access_token) return data; // { access_token, refresh_token, expires_in }
|
|
72
|
+
|
|
73
|
+
if (data.error === 'authorization_pending') {
|
|
74
|
+
// normal — keep waiting
|
|
75
|
+
} else if (data.error === 'slow_down') {
|
|
76
|
+
interval += 5 + Math.random() * 2; // RFC 8628 §3.5 — add jitter to avoid lockstep
|
|
77
|
+
} else if (data.error === 'expired_token') {
|
|
78
|
+
throw new Error('Login timed out — please run velaro login again.');
|
|
79
|
+
} else if (data.error === 'access_denied') {
|
|
80
|
+
throw new Error('Login cancelled.');
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error(data.error_description || data.error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await sleep(interval * 1000); // sleep at end so first attempt fires immediately
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function exchangeForVelaroToken(entraAccessToken, apiBase) {
|
|
90
|
+
const res = await fetch(`${apiBase}/auth/entra/token`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { Authorization: `Bearer ${entraAccessToken}` },
|
|
93
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const body = await res.text().catch(() => res.status.toString());
|
|
98
|
+
throw new Error(`Velaro token exchange failed (${res.status}): ${body}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return res.json(); // { token: { token, expires }, profile: { SiteId, Name, UserName, ... } }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function refreshVelaroToken(credentials) {
|
|
105
|
+
const res = await fetch(TOKEN_URL, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
108
|
+
body: new URLSearchParams({
|
|
109
|
+
grant_type: 'refresh_token',
|
|
110
|
+
client_id: CLIENT_ID,
|
|
111
|
+
refresh_token: credentials.entraRefreshToken,
|
|
112
|
+
}),
|
|
113
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const data = await res.json();
|
|
117
|
+
if (!data.access_token) {
|
|
118
|
+
throw new Error(`Token refresh failed: ${data.error_description || data.error}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const velaro = await exchangeForVelaroToken(data.access_token, credentials.apiBase);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
...credentials,
|
|
125
|
+
velaroToken: velaro.token.token,
|
|
126
|
+
velaroExpires: velaro.token.expires,
|
|
127
|
+
entraRefreshToken: data.refresh_token || credentials.entraRefreshToken,
|
|
128
|
+
siteId: velaro.profile?.SiteId ?? credentials.siteId,
|
|
129
|
+
userName: velaro.profile?.Name ?? credentials.userName,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sleep(ms) {
|
|
134
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
|
+
}
|
package/lib/run.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { track } from './track.js';
|
|
2
|
-
|
|
3
|
-
/** Wraps a yargs command handler with consistent error reporting and usage tracking. */
|
|
4
|
-
export function runCommand(fn) {
|
|
5
|
-
return async (argv) => {
|
|
6
|
-
try {
|
|
7
|
-
await fn(argv);
|
|
8
|
-
// Fire-and-forget tracking after successful command — never awaited, never blocks
|
|
9
|
-
const action = argv._.join('.') || 'unknown';
|
|
10
|
-
track(action);
|
|
11
|
-
} catch (err) {
|
|
12
|
-
console.error(`Error: ${err.message}`);
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
}
|
|
1
|
+
import { track } from './track.js';
|
|
2
|
+
|
|
3
|
+
/** Wraps a yargs command handler with consistent error reporting and usage tracking. */
|
|
4
|
+
export function runCommand(fn) {
|
|
5
|
+
return async (argv) => {
|
|
6
|
+
try {
|
|
7
|
+
await fn(argv);
|
|
8
|
+
// Fire-and-forget tracking after successful command — never awaited, never blocks
|
|
9
|
+
const action = argv._.join('.') || 'unknown';
|
|
10
|
+
track(action);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
console.error(`Error: ${err.message}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
package/lib/subscription.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription helpers for CLI commands.
|
|
3
|
-
* Provides a clean gate that prints a plan upgrade message instead of
|
|
4
|
-
* throwing a confusing 403/400 from the API.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { get } from './api.js';
|
|
8
|
-
|
|
9
|
-
let _cachedSub = null;
|
|
10
|
-
|
|
11
|
-
export async function getSubscription() {
|
|
12
|
-
if (!_cachedSub) {
|
|
13
|
-
_cachedSub = await get('/Subscription');
|
|
14
|
-
}
|
|
15
|
-
return _cachedSub;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Throws an Error with a friendly upgrade message if the feature flag is off.
|
|
20
|
-
* Usage: await requireFeature('enableAI', 'AI Bots');
|
|
21
|
-
*/
|
|
22
|
-
export async function requireFeature(flag, featureName) {
|
|
23
|
-
const sub = await getSubscription();
|
|
24
|
-
if (!sub[flag]) {
|
|
25
|
-
throw new Error(
|
|
26
|
-
`${featureName} is not enabled on your plan.\n` +
|
|
27
|
-
` Upgrade at https://velaro.com/pricing or contact sales@velaro.com.`
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
return sub;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Returns true/false without throwing — for check/status commands.
|
|
35
|
-
*/
|
|
36
|
-
export async function hasFeature(flag) {
|
|
37
|
-
const sub = await getSubscription();
|
|
38
|
-
return sub[flag] === true;
|
|
39
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Subscription helpers for CLI commands.
|
|
3
|
+
* Provides a clean gate that prints a plan upgrade message instead of
|
|
4
|
+
* throwing a confusing 403/400 from the API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { get } from './api.js';
|
|
8
|
+
|
|
9
|
+
let _cachedSub = null;
|
|
10
|
+
|
|
11
|
+
export async function getSubscription() {
|
|
12
|
+
if (!_cachedSub) {
|
|
13
|
+
_cachedSub = await get('/Subscription');
|
|
14
|
+
}
|
|
15
|
+
return _cachedSub;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Throws an Error with a friendly upgrade message if the feature flag is off.
|
|
20
|
+
* Usage: await requireFeature('enableAI', 'AI Bots');
|
|
21
|
+
*/
|
|
22
|
+
export async function requireFeature(flag, featureName) {
|
|
23
|
+
const sub = await getSubscription();
|
|
24
|
+
if (!sub[flag]) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`${featureName} is not enabled on your plan.\n` +
|
|
27
|
+
` Upgrade at https://velaro.com/pricing or contact sales@velaro.com.`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return sub;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns true/false without throwing — for check/status commands.
|
|
35
|
+
*/
|
|
36
|
+
export async function hasFeature(flag) {
|
|
37
|
+
const sub = await getSubscription();
|
|
38
|
+
return sub[flag] === true;
|
|
39
|
+
}
|
package/lib/track.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fire-and-forget CLI usage tracker.
|
|
3
|
-
* Posts a single event to /ToolUsage/cli after each command.
|
|
4
|
-
* Never throws, never blocks — silently dropped on any failure.
|
|
5
|
-
*/
|
|
6
|
-
import { readConfig } from './config.js';
|
|
7
|
-
import { createRequire } from 'module';
|
|
8
|
-
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
10
|
-
const { version: CLI_VERSION } = require('../package.json');
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Track a CLI action. Call after a command succeeds.
|
|
14
|
-
* @param {string} action e.g. "login", "bot.push", "workflow.pull"
|
|
15
|
-
*/
|
|
16
|
-
export function track(action) {
|
|
17
|
-
setImmediate(async () => {
|
|
18
|
-
try {
|
|
19
|
-
const creds = readConfig();
|
|
20
|
-
if (!creds?.velaroToken || !creds?.apiBase) return;
|
|
21
|
-
|
|
22
|
-
await fetch(`${creds.apiBase}/ToolUsage/cli`, {
|
|
23
|
-
method: 'POST',
|
|
24
|
-
headers: {
|
|
25
|
-
Authorization: `Bearer ${creds.velaroToken}`,
|
|
26
|
-
'Content-Type': 'application/json',
|
|
27
|
-
},
|
|
28
|
-
body: JSON.stringify({ action, cliVersion: CLI_VERSION }),
|
|
29
|
-
signal: AbortSignal.timeout(3000),
|
|
30
|
-
});
|
|
31
|
-
} catch {
|
|
32
|
-
// Silently drop — tracking must never surface errors to the user
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget CLI usage tracker.
|
|
3
|
+
* Posts a single event to /ToolUsage/cli after each command.
|
|
4
|
+
* Never throws, never blocks — silently dropped on any failure.
|
|
5
|
+
*/
|
|
6
|
+
import { readConfig } from './config.js';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { version: CLI_VERSION } = require('../package.json');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Track a CLI action. Call after a command succeeds.
|
|
14
|
+
* @param {string} action e.g. "login", "bot.push", "workflow.pull"
|
|
15
|
+
*/
|
|
16
|
+
export function track(action) {
|
|
17
|
+
setImmediate(async () => {
|
|
18
|
+
try {
|
|
19
|
+
const creds = readConfig();
|
|
20
|
+
if (!creds?.velaroToken || !creds?.apiBase) return;
|
|
21
|
+
|
|
22
|
+
await fetch(`${creds.apiBase}/ToolUsage/cli`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({ action, cliVersion: CLI_VERSION }),
|
|
29
|
+
signal: AbortSignal.timeout(3000),
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
// Silently drop — tracking must never surface errors to the user
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|