@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.
Files changed (98) hide show
  1. package/README.md +161 -138
  2. package/bin/velaro.js +177 -62
  3. package/lib/api.js +91 -52
  4. package/lib/api.test.js +46 -0
  5. package/lib/banner.js +76 -0
  6. package/lib/commands/activity.js +133 -0
  7. package/lib/commands/acuity.js +66 -0
  8. package/lib/commands/agent.js +204 -50
  9. package/lib/commands/ai-config.js +193 -0
  10. package/lib/commands/ai-models.js +159 -0
  11. package/lib/commands/appointments.js +198 -0
  12. package/lib/commands/article.js +668 -388
  13. package/lib/commands/automation-draft.js +134 -0
  14. package/lib/commands/avatar.js +75 -0
  15. package/lib/commands/bigcommerce.js +50 -0
  16. package/lib/commands/billing-contacts.js +62 -0
  17. package/lib/commands/billing-email-preference.js +64 -0
  18. package/lib/commands/billing-subscription.js +265 -0
  19. package/lib/commands/billing.js +138 -0
  20. package/lib/commands/bot.js +141 -137
  21. package/lib/commands/bundle.js +168 -0
  22. package/lib/commands/calendly.js +62 -0
  23. package/lib/commands/callback.js +125 -0
  24. package/lib/commands/callrail.js +88 -0
  25. package/lib/commands/campaigns.js +44 -0
  26. package/lib/commands/case.js +102 -0
  27. package/lib/commands/check.js +163 -163
  28. package/lib/commands/compliance.js +229 -0
  29. package/lib/commands/conversation-efficiency.js +178 -0
  30. package/lib/commands/copilotstudio.js +114 -0
  31. package/lib/commands/coupon-grant.js +192 -0
  32. package/lib/commands/db.js +101 -0
  33. package/lib/commands/deployment.js +107 -107
  34. package/lib/commands/diagnostics.js +298 -0
  35. package/lib/commands/email-campaign.js +47 -0
  36. package/lib/commands/email-inbox.js +88 -0
  37. package/lib/commands/entitlement.js +176 -0
  38. package/lib/commands/env.js +45 -45
  39. package/lib/commands/feature-discovery.js +40 -0
  40. package/lib/commands/focus.js +278 -0
  41. package/lib/commands/index.js +38 -5
  42. package/lib/commands/ingest.js +31 -31
  43. package/lib/commands/inline-widget-config.js +126 -0
  44. package/lib/commands/integration.js +93 -0
  45. package/lib/commands/kb.js +450 -309
  46. package/lib/commands/login.js +86 -86
  47. package/lib/commands/logs.js +680 -0
  48. package/lib/commands/magento.js +210 -0
  49. package/lib/commands/mcp-key.js +188 -159
  50. package/lib/commands/migrate.js +134 -0
  51. package/lib/commands/migration-status.js +66 -0
  52. package/lib/commands/monday.js +137 -0
  53. package/lib/commands/netsuite.js +87 -0
  54. package/lib/commands/notifications.js +63 -0
  55. package/lib/commands/notion.js +70 -0
  56. package/lib/commands/ops.js +267 -173
  57. package/lib/commands/payment-recovery.js +170 -0
  58. package/lib/commands/pickup.js +172 -0
  59. package/lib/commands/pricing.js +132 -0
  60. package/lib/commands/product.js +55 -0
  61. package/lib/commands/recruiting.js +374 -0
  62. package/lib/commands/report.js +462 -0
  63. package/lib/commands/routing.js +304 -0
  64. package/lib/commands/rule.js +85 -85
  65. package/lib/commands/sharepoint.js +167 -0
  66. package/lib/commands/site-provision.js +68 -0
  67. package/lib/commands/site.js +62 -62
  68. package/lib/commands/sitesync.js +158 -0
  69. package/lib/commands/slack.js +64 -0
  70. package/lib/commands/squarespace.js +108 -0
  71. package/lib/commands/status.js +24 -24
  72. package/lib/commands/subscription.js +43 -0
  73. package/lib/commands/support.js +128 -0
  74. package/lib/commands/survey.js +216 -0
  75. package/lib/commands/team.js +144 -144
  76. package/lib/commands/teams-phone.js +131 -0
  77. package/lib/commands/teams.js +106 -0
  78. package/lib/commands/telephony.js +99 -0
  79. package/lib/commands/update.js +47 -47
  80. package/lib/commands/webflow.js +128 -0
  81. package/lib/commands/whoami.js +25 -22
  82. package/lib/commands/widget-container.js +152 -0
  83. package/lib/commands/woocommerce.js +240 -0
  84. package/lib/commands/workflow.js +233 -98
  85. package/lib/config.js +85 -83
  86. package/lib/kb-screenshot.js +320 -0
  87. package/lib/migrations/amscro.json +72 -0
  88. package/lib/migrations/azenta.json +68 -0
  89. package/lib/migrations/bluefire.json +49 -0
  90. package/lib/migrations/donaldson.json +75 -0
  91. package/lib/oauth.js +149 -135
  92. package/lib/run.js +21 -16
  93. package/lib/sharepoint-auth.js +138 -0
  94. package/lib/subscription.js +41 -39
  95. package/lib/track.js +35 -35
  96. package/lib/update-check.js +64 -64
  97. package/package.json +34 -19
  98. 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
- 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
+ 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 loginretry 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
+ }
@@ -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 { 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 { 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
+ }