@velaro/cli 1.2.0 → 1.4.8
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 +161 -138
- package/bin/velaro.js +177 -62
- package/lib/api.js +91 -52
- package/lib/api.test.js +46 -0
- package/lib/banner.js +76 -0
- package/lib/commands/activity.js +133 -0
- package/lib/commands/acuity.js +66 -0
- package/lib/commands/agent.js +204 -50
- package/lib/commands/ai-config.js +193 -0
- package/lib/commands/ai-models.js +159 -0
- package/lib/commands/appointments.js +198 -0
- package/lib/commands/article.js +668 -388
- package/lib/commands/automation-draft.js +134 -0
- package/lib/commands/avatar.js +75 -0
- package/lib/commands/bigcommerce.js +50 -0
- package/lib/commands/billing-contacts.js +62 -0
- package/lib/commands/billing-email-preference.js +64 -0
- package/lib/commands/billing-subscription.js +265 -0
- package/lib/commands/billing.js +138 -0
- package/lib/commands/bot.js +141 -137
- package/lib/commands/bundle.js +168 -0
- package/lib/commands/calendly.js +62 -0
- package/lib/commands/callback.js +125 -0
- package/lib/commands/callrail.js +88 -0
- package/lib/commands/campaigns.js +44 -0
- package/lib/commands/case.js +102 -0
- package/lib/commands/check.js +163 -163
- package/lib/commands/compliance.js +229 -0
- package/lib/commands/conversation-efficiency.js +178 -0
- package/lib/commands/copilotstudio.js +114 -0
- package/lib/commands/coupon-grant.js +192 -0
- package/lib/commands/db.js +101 -0
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/diagnostics.js +298 -0
- package/lib/commands/email-campaign.js +47 -0
- package/lib/commands/email-inbox.js +88 -0
- package/lib/commands/entitlement.js +176 -0
- package/lib/commands/env.js +45 -45
- package/lib/commands/feature-discovery.js +40 -0
- package/lib/commands/focus.js +278 -0
- package/lib/commands/index.js +38 -5
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/inline-widget-config.js +126 -0
- package/lib/commands/integration.js +93 -0
- package/lib/commands/kb.js +450 -309
- package/lib/commands/login.js +86 -86
- package/lib/commands/logs.js +680 -0
- package/lib/commands/magento.js +210 -0
- package/lib/commands/mcp-key.js +188 -159
- package/lib/commands/migrate.js +134 -0
- package/lib/commands/migration-status.js +66 -0
- package/lib/commands/monday.js +137 -0
- package/lib/commands/netsuite.js +87 -0
- package/lib/commands/notifications.js +63 -0
- package/lib/commands/notion.js +70 -0
- package/lib/commands/ops.js +267 -173
- package/lib/commands/payment-recovery.js +170 -0
- package/lib/commands/pickup.js +172 -0
- package/lib/commands/pricing.js +132 -0
- package/lib/commands/product.js +55 -0
- package/lib/commands/recruiting.js +374 -0
- package/lib/commands/report.js +462 -0
- package/lib/commands/routing.js +304 -0
- package/lib/commands/rule.js +85 -85
- package/lib/commands/sharepoint.js +167 -0
- package/lib/commands/site-provision.js +68 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/sitesync.js +158 -0
- package/lib/commands/slack.js +64 -0
- package/lib/commands/squarespace.js +108 -0
- package/lib/commands/status.js +24 -24
- package/lib/commands/subscription.js +43 -0
- package/lib/commands/support.js +128 -0
- package/lib/commands/survey.js +216 -0
- package/lib/commands/team.js +144 -144
- package/lib/commands/teams-phone.js +131 -0
- package/lib/commands/teams.js +106 -0
- package/lib/commands/telephony.js +99 -0
- package/lib/commands/update.js +47 -47
- package/lib/commands/webflow.js +128 -0
- package/lib/commands/whoami.js +25 -22
- package/lib/commands/widget-container.js +152 -0
- package/lib/commands/woocommerce.js +240 -0
- package/lib/commands/workflow.js +233 -98
- package/lib/config.js +85 -83
- package/lib/kb-screenshot.js +320 -0
- package/lib/migrations/amscro.json +72 -0
- package/lib/migrations/azenta.json +68 -0
- package/lib/migrations/bluefire.json +49 -0
- package/lib/migrations/donaldson.json +75 -0
- package/lib/oauth.js +149 -135
- package/lib/run.js +21 -16
- package/lib/sharepoint-auth.js +138 -0
- package/lib/subscription.js +41 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +34 -19
- package/scripts/postinstall.js +12 -0
package/lib/oauth.js
CHANGED
|
@@ -1,135 +1,149 @@
|
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
let consecutiveTransientFailures = 0;
|
|
57
|
+
const MAX_CONSECUTIVE_TRANSIENT_FAILURES = 6; // ~30-60s of real outage, not one slow response
|
|
58
|
+
|
|
59
|
+
while (true) {
|
|
60
|
+
let data;
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(TOKEN_URL, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
65
|
+
body: new URLSearchParams({
|
|
66
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
67
|
+
client_id: CLIENT_ID,
|
|
68
|
+
device_code: deviceCode,
|
|
69
|
+
}),
|
|
70
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
71
|
+
});
|
|
72
|
+
data = await res.json();
|
|
73
|
+
consecutiveTransientFailures = 0;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
// A single slow response or network blip during the 30-60s the user needs to complete
|
|
76
|
+
// the browser step must not kill the whole login — retry like any other pending poll.
|
|
77
|
+
consecutiveTransientFailures++;
|
|
78
|
+
if (consecutiveTransientFailures >= MAX_CONSECUTIVE_TRANSIENT_FAILURES) {
|
|
79
|
+
throw new Error(`Login polling failed repeatedly (${err.message}) — check your network and run velaro login again.`);
|
|
80
|
+
}
|
|
81
|
+
await sleep(interval * 1000);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (data.access_token) return data; // { access_token, refresh_token, expires_in }
|
|
86
|
+
|
|
87
|
+
if (data.error === 'authorization_pending') {
|
|
88
|
+
// normal — keep waiting
|
|
89
|
+
} else if (data.error === 'slow_down') {
|
|
90
|
+
interval += 5 + Math.random() * 2; // RFC 8628 §3.5 — add jitter to avoid lockstep
|
|
91
|
+
} else if (data.error === 'expired_token') {
|
|
92
|
+
throw new Error('Login timed out — please run velaro login again.');
|
|
93
|
+
} else if (data.error === 'access_denied') {
|
|
94
|
+
throw new Error('Login cancelled.');
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error(data.error_description || data.error);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await sleep(interval * 1000); // sleep at end so first attempt fires immediately
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function exchangeForVelaroToken(entraAccessToken, apiBase) {
|
|
104
|
+
const res = await fetch(`${apiBase}/auth/entra/token`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { Authorization: `Bearer ${entraAccessToken}` },
|
|
107
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const body = await res.text().catch(() => res.status.toString());
|
|
112
|
+
throw new Error(`Velaro token exchange failed (${res.status}): ${body}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return res.json(); // { token: { token, expires }, profile: { siteId, userName, ... } } -- camelCase, matches ASP.NET Core's default JSON casing
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function refreshVelaroToken(credentials) {
|
|
119
|
+
const res = await fetch(TOKEN_URL, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
122
|
+
body: new URLSearchParams({
|
|
123
|
+
grant_type: 'refresh_token',
|
|
124
|
+
client_id: CLIENT_ID,
|
|
125
|
+
refresh_token: credentials.entraRefreshToken,
|
|
126
|
+
}),
|
|
127
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
if (!data.access_token) {
|
|
132
|
+
throw new Error(`Token refresh failed: ${data.error_description || data.error}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const velaro = await exchangeForVelaroToken(data.access_token, credentials.apiBase);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
...credentials,
|
|
139
|
+
velaroToken: velaro.token.token,
|
|
140
|
+
velaroExpires: velaro.token.expires,
|
|
141
|
+
entraRefreshToken: data.refresh_token || credentials.entraRefreshToken,
|
|
142
|
+
siteId: velaro.profile?.siteId ?? credentials.siteId,
|
|
143
|
+
userName: velaro.profile?.userName ?? credentials.userName,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sleep(ms) {
|
|
148
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
149
|
+
}
|
package/lib/run.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
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) here raced pending handles (open fetch keep-alive sockets, unflushed
|
|
14
|
+
// stdout) and intermittently crashed with a libuv assertion on Windows
|
|
15
|
+
// ("Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c").
|
|
16
|
+
// process.exitCode lets the event loop drain naturally before Node exits with the same
|
|
17
|
+
// code -- same pattern already used in bigcommerce.js/copilotstudio.js/monday.js/notion.js.
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SharePoint / Microsoft Graph auth for the Velaro CLI.
|
|
3
|
+
*
|
|
4
|
+
* Uses a separate device-code flow against the nocvelaro M365 tenant
|
|
5
|
+
* (NOT the Velaro CIAM tenant — completely different auth system).
|
|
6
|
+
*
|
|
7
|
+
* Tokens stored under config.sharepoint.* in ~/.velaro/config.json.
|
|
8
|
+
* Run once: velaro sharepoint login
|
|
9
|
+
* Then: velaro sharepoint read-tracker
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const TENANT_ID = 'e796b25e-62d7-42c0-bbd0-e8ebd7d972e4'; // nocvelaro.sharepoint.com
|
|
13
|
+
const AUTHORITY = `https://login.microsoftonline.com/${TENANT_ID}`;
|
|
14
|
+
// Public client app ID — must be registered in nocvelaro tenant with Files.Read delegated consent.
|
|
15
|
+
// Use the Velaro CLI app or a dedicated "Velaro CLI SharePoint" app.
|
|
16
|
+
// Override via VELARO_SP_CLIENT_ID env var.
|
|
17
|
+
const CLIENT_ID = process.env.VELARO_SP_CLIENT_ID || 'c0fdce54-e9b4-4427-a021-b2605855ff8b';
|
|
18
|
+
|
|
19
|
+
const SCOPE = [
|
|
20
|
+
'https://graph.microsoft.com/Files.Read',
|
|
21
|
+
'https://graph.microsoft.com/Sites.Read.All',
|
|
22
|
+
'offline_access',
|
|
23
|
+
].join(' ');
|
|
24
|
+
|
|
25
|
+
const DEVICE_CODE_URL = `${AUTHORITY}/oauth2/v2.0/devicecode`;
|
|
26
|
+
const TOKEN_URL = `${AUTHORITY}/oauth2/v2.0/token`;
|
|
27
|
+
const FETCH_TIMEOUT = 15_000;
|
|
28
|
+
|
|
29
|
+
function sleep(ms) {
|
|
30
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function spRequestDeviceCode() {
|
|
34
|
+
const res = await fetch(DEVICE_CODE_URL, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
37
|
+
body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPE }),
|
|
38
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
39
|
+
});
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
if (data.error) throw new Error(`SP device code failed: ${data.error_description || data.error}`);
|
|
42
|
+
return data; // { device_code, user_code, verification_uri, expires_in, interval, message }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function spPollForToken(deviceCode, intervalSeconds) {
|
|
46
|
+
let interval = intervalSeconds;
|
|
47
|
+
while (true) {
|
|
48
|
+
const res = await fetch(TOKEN_URL, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
51
|
+
body: new URLSearchParams({
|
|
52
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
53
|
+
client_id: CLIENT_ID,
|
|
54
|
+
device_code: deviceCode,
|
|
55
|
+
}),
|
|
56
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
57
|
+
});
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
if (data.access_token) return data;
|
|
60
|
+
if (data.error === 'authorization_pending') {
|
|
61
|
+
// keep waiting
|
|
62
|
+
} else if (data.error === 'slow_down') {
|
|
63
|
+
interval += 5;
|
|
64
|
+
} else if (data.error === 'expired_token') {
|
|
65
|
+
throw new Error('Login timed out — run velaro sharepoint login again.');
|
|
66
|
+
} else if (data.error === 'access_denied') {
|
|
67
|
+
throw new Error('Login cancelled.');
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error(data.error_description || data.error);
|
|
70
|
+
}
|
|
71
|
+
await sleep(interval * 1000);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function spRefreshToken(refreshToken) {
|
|
76
|
+
const res = await fetch(TOKEN_URL, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
79
|
+
body: new URLSearchParams({
|
|
80
|
+
grant_type: 'refresh_token',
|
|
81
|
+
client_id: CLIENT_ID,
|
|
82
|
+
refresh_token: refreshToken,
|
|
83
|
+
scope: SCOPE,
|
|
84
|
+
}),
|
|
85
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
86
|
+
});
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
if (!data.access_token) {
|
|
89
|
+
throw new Error(`SP token refresh failed: ${data.error_description || data.error}`);
|
|
90
|
+
}
|
|
91
|
+
return data; // { access_token, refresh_token, expires_in }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns a valid SharePoint access token, refreshing silently if needed.
|
|
96
|
+
* Throws if not logged in — caller should catch and prompt velaro sharepoint login.
|
|
97
|
+
*/
|
|
98
|
+
export async function getSpAccessToken(cfg) {
|
|
99
|
+
const sp = cfg.sharepoint;
|
|
100
|
+
if (!sp?.refreshToken) {
|
|
101
|
+
throw new Error('Not logged into SharePoint. Run: velaro sharepoint login');
|
|
102
|
+
}
|
|
103
|
+
const expiresAt = sp.expiresAt ? new Date(sp.expiresAt) : new Date(0);
|
|
104
|
+
const needsRefresh = Date.now() >= expiresAt.getTime() - 5 * 60 * 1000;
|
|
105
|
+
if (!needsRefresh && sp.accessToken) return { accessToken: sp.accessToken, refreshed: null };
|
|
106
|
+
|
|
107
|
+
const data = await spRefreshToken(sp.refreshToken);
|
|
108
|
+
const newSp = {
|
|
109
|
+
accessToken: data.access_token,
|
|
110
|
+
refreshToken: data.refresh_token || sp.refreshToken,
|
|
111
|
+
expiresAt: new Date(Date.now() + data.expires_in * 1000).toISOString(),
|
|
112
|
+
};
|
|
113
|
+
return { accessToken: data.access_token, refreshed: newSp };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Downloads a file from SharePoint/OneDrive via Graph API using the file's web URL.
|
|
118
|
+
* Returns the raw Buffer (binary content).
|
|
119
|
+
*/
|
|
120
|
+
export async function downloadSharePointFile(webUrl, accessToken) {
|
|
121
|
+
// Graph endpoint: /v1.0/shares/{encoded-url}/driveItem/content
|
|
122
|
+
const encoded = Buffer.from(`u!${webUrl}`).toString('base64url');
|
|
123
|
+
const graphUrl = `https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem/content`;
|
|
124
|
+
|
|
125
|
+
const res = await fetch(graphUrl, {
|
|
126
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
127
|
+
signal: AbortSignal.timeout(30_000),
|
|
128
|
+
redirect: 'follow',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
const body = await res.text().catch(() => '');
|
|
133
|
+
throw new Error(`Graph download failed (${res.status}): ${body.slice(0, 300)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const buf = await res.arrayBuffer();
|
|
137
|
+
return Buffer.from(buf);
|
|
138
|
+
}
|
package/lib/subscription.js
CHANGED
|
@@ -1,39 +1,41 @@
|
|
|
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 {
|
|
8
|
-
|
|
9
|
-
let _cachedSub = null;
|
|
10
|
-
|
|
11
|
-
export async function getSubscription() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 { messagingGet } from './api.js';
|
|
8
|
+
|
|
9
|
+
let _cachedSub = null;
|
|
10
|
+
|
|
11
|
+
export async function getSubscription() {
|
|
12
|
+
// SubscriptionController lives in velaro-messaging, not velaro-admin -- this was calling the
|
|
13
|
+
// admin API and 404ing on every CLI command that gates on a feature flag (e.g. `kb qna list`).
|
|
14
|
+
if (!_cachedSub) {
|
|
15
|
+
_cachedSub = await messagingGet('/Subscription');
|
|
16
|
+
}
|
|
17
|
+
return _cachedSub;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Throws an Error with a friendly upgrade message if the feature flag is off.
|
|
22
|
+
* Usage: await requireFeature('enableAI', 'AI Bots');
|
|
23
|
+
*/
|
|
24
|
+
export async function requireFeature(flag, featureName) {
|
|
25
|
+
const sub = await getSubscription();
|
|
26
|
+
if (!sub[flag]) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`${featureName} is not enabled on your plan.\n` +
|
|
29
|
+
` Upgrade at https://velaro.com/pricing or contact sales@velaro.com.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return sub;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns true/false without throwing — for check/status commands.
|
|
37
|
+
*/
|
|
38
|
+
export async function hasFeature(flag) {
|
|
39
|
+
const sub = await getSubscription();
|
|
40
|
+
return sub[flag] === true;
|
|
41
|
+
}
|
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
|
+
}
|