@vizzly-testing/cli 0.39.0 → 0.39.2

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 (195) hide show
  1. package/README.md +12 -3
  2. package/bin/vizzly.js +1 -1
  3. package/dist/{cli.js → adapters/cli/cli.js} +50 -35
  4. package/dist/adapters/cli/setup.js +34 -0
  5. package/dist/{client → adapters/client}/index.js +6 -7
  6. package/dist/adapters/http/cloud-proxy.js +29 -0
  7. package/dist/{server → adapters/http}/http-server.js +12 -13
  8. package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
  9. package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
  10. package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
  11. package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
  12. package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
  13. package/dist/adapters/sdk/index.d.ts +89 -0
  14. package/dist/{sdk → adapters/sdk}/index.js +22 -19
  15. package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
  16. package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
  17. package/dist/domains/auth/auth-errors.js +50 -0
  18. package/dist/domains/auth/auth-headers.js +41 -0
  19. package/dist/domains/auth/auth.normalizers.js +50 -0
  20. package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
  21. package/dist/domains/auth/auth.rules.js +19 -0
  22. package/dist/domains/auth/auth.serializers.js +32 -0
  23. package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
  24. package/dist/domains/auth/auth.validations.js +21 -0
  25. package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
  26. package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
  27. package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
  28. package/dist/domains/auth/token-store.js +81 -0
  29. package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
  30. package/dist/domains/baselines/baseline-download.js +192 -0
  31. package/dist/domains/baselines/baseline-write.js +22 -0
  32. package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
  33. package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
  34. package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
  35. package/dist/domains/baselines/cloud-baselines.js +9 -0
  36. package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
  37. package/dist/domains/builds/build.normalizers.js +35 -0
  38. package/dist/domains/builds/build.serializers.js +164 -0
  39. package/dist/domains/builds/build.service.js +119 -0
  40. package/dist/domains/builds/build.validations.js +39 -0
  41. package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
  42. package/dist/domains/builds/cloud-builds.routes.js +31 -0
  43. package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
  44. package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
  45. package/dist/domains/comparisons/cloud-comparisons.js +43 -0
  46. package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
  47. package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
  48. package/dist/domains/config/config-defaults.js +27 -0
  49. package/dist/{utils → domains/config}/config-discovery.js +1 -9
  50. package/dist/{utils → domains/config}/config-loader.js +10 -9
  51. package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
  52. package/dist/{types → domains/config}/config.d.ts +3 -3
  53. package/dist/domains/config/config.normalizers.js +126 -0
  54. package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
  55. package/dist/domains/config/config.rules.js +47 -0
  56. package/dist/domains/config/config.serializers.js +202 -0
  57. package/dist/domains/config/config.service.js +625 -0
  58. package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
  59. package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
  60. package/dist/domains/context/compact-context.validations.js +60 -0
  61. package/dist/domains/context/context-presentation.js +368 -0
  62. package/dist/domains/context/context.commands.js +385 -0
  63. package/dist/domains/context/context.serializers.js +157 -0
  64. package/dist/domains/context/context.service.js +76 -0
  65. package/dist/domains/context/context.validations.js +100 -0
  66. package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
  67. package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
  68. package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
  69. package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
  70. package/dist/domains/previews/preview.service.js +37 -0
  71. package/dist/domains/projects/cloud-projects.routes.js +28 -0
  72. package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
  73. package/dist/{utils → domains/projects}/project-binding.js +3 -3
  74. package/dist/domains/projects/project-errors.js +61 -0
  75. package/dist/{utils → domains/projects}/project-link-store.js +2 -2
  76. package/dist/domains/projects/project-requests.js +61 -0
  77. package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
  78. package/dist/domains/projects/project.normalizers.js +69 -0
  79. package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
  80. package/dist/domains/projects/project.validations.js +31 -0
  81. package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
  82. package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
  83. package/dist/domains/reports/dashboard.routes.js +96 -0
  84. package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
  85. package/dist/domains/reports/report-assets.routes.js +81 -0
  86. package/dist/domains/reports/report-state.js +51 -0
  87. package/dist/domains/reports/report-store.js +85 -0
  88. package/dist/domains/reports/report.serializers.js +99 -0
  89. package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
  90. package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
  91. package/dist/domains/runs/run-environment.js +69 -0
  92. package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
  93. package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
  94. package/dist/domains/runs/run.normalizers.js +8 -0
  95. package/dist/domains/runs/run.rules.js +56 -0
  96. package/dist/domains/runs/run.serializers.js +101 -0
  97. package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
  98. package/dist/domains/runs/run.validations.js +17 -0
  99. package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
  100. package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
  101. package/dist/domains/screenshots/cloud-screenshots.js +109 -0
  102. package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
  103. package/dist/domains/screenshots/screenshot-hash.js +53 -0
  104. package/dist/domains/screenshots/screenshot-server-options.js +28 -0
  105. package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
  106. package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
  107. package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
  108. package/dist/domains/screenshots/screenshot.serializers.js +146 -0
  109. package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
  110. package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
  111. package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
  112. package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
  113. package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
  114. package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
  115. package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
  116. package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
  117. package/dist/domains/updates/cli-update.js +103 -0
  118. package/dist/domains/updates/update-check.js +2 -0
  119. package/dist/domains/updates/update-notifications.js +175 -0
  120. package/dist/domains/uploads/upload-defaults.js +4 -0
  121. package/dist/domains/uploads/upload-files.js +32 -0
  122. package/dist/domains/uploads/upload-settings.js +21 -0
  123. package/dist/domains/uploads/upload-timing.js +18 -0
  124. package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
  125. package/dist/domains/uploads/upload.normalizers.js +60 -0
  126. package/dist/domains/uploads/upload.serializers.js +179 -0
  127. package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
  128. package/dist/domains/uploads/upload.validations.js +77 -0
  129. package/dist/index.js +12 -12
  130. package/dist/reporter/reporter-bundle.iife.js +86 -86
  131. package/dist/reporter-ssr/ssr-entry.js +3 -6
  132. package/dist/shared/cloud-protocol.js +171 -0
  133. package/dist/{utils → shared}/file-changes.js +1 -1
  134. package/dist/shared/files.js +9 -0
  135. package/dist/{utils → shared}/global-config.js +7 -78
  136. package/dist/shared/log-level.js +7 -0
  137. package/dist/{utils → shared}/output.js +1 -1
  138. package/dist/{utils → shared}/package-info.js +5 -4
  139. package/dist/shared/paths.js +57 -0
  140. package/package.json +37 -36
  141. package/dist/api/core.js +0 -382
  142. package/dist/api/endpoints.js +0 -421
  143. package/dist/api/index.js +0 -19
  144. package/dist/auth/core.js +0 -237
  145. package/dist/auth/index.js +0 -31
  146. package/dist/commands/context.js +0 -1066
  147. package/dist/config/core.js +0 -439
  148. package/dist/config/operations.js +0 -327
  149. package/dist/project/core.js +0 -211
  150. package/dist/screenshot-server/core.js +0 -161
  151. package/dist/screenshot-server/index.js +0 -11
  152. package/dist/server/routers/assets.js +0 -95
  153. package/dist/server/routers/cloud-proxy.js +0 -146
  154. package/dist/server/routers/dashboard.js +0 -172
  155. package/dist/services/auth-service.js +0 -117
  156. package/dist/services/config-service.js +0 -313
  157. package/dist/services/project-service.js +0 -88
  158. package/dist/test-runner/core.js +0 -293
  159. package/dist/test-runner/index.js +0 -13
  160. package/dist/types/sdk.d.ts +0 -186
  161. package/dist/uploader/core.js +0 -432
  162. package/dist/uploader/index.js +0 -88
  163. /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
  164. /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
  165. /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
  166. /package/dist/{server → adapters/http}/middleware/response.js +0 -0
  167. /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
  168. /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
  169. /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
  170. /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
  171. /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
  172. /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
  173. /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
  174. /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
  175. /package/dist/{context → domains/context}/provider-resolver.js +0 -0
  176. /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
  177. /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
  178. /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
  179. /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
  180. /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
  181. /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
  182. /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
  183. /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
  184. /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
  185. /package/dist/{types/index.d.ts → index.d.ts} +0 -0
  186. /package/dist/{utils → shared}/api-url.js +0 -0
  187. /package/dist/{utils → shared}/async-utils.js +0 -0
  188. /package/dist/{utils → shared}/browser.js +0 -0
  189. /package/dist/{utils → shared}/ci-env.js +0 -0
  190. /package/dist/{utils → shared}/colors.js +0 -0
  191. /package/dist/{utils → shared}/fetch-utils.js +0 -0
  192. /package/dist/{utils → shared}/git.js +0 -0
  193. /package/dist/{utils → shared}/patterns.js +0 -0
  194. /package/dist/{utils → shared}/secret-store.js +0 -0
  195. /package/dist/{errors → shared}/vizzly-error.js +0 -0
@@ -1,12 +1,13 @@
1
+ import { getPackageVersion } from '../../shared/package-info.js';
2
+ import { parseAuthError, parseAuthenticatedError } from './auth-errors.js';
3
+ import { buildAuthUserAgent, buildRequestHeaders } from './auth-headers.js';
4
+
1
5
  /**
2
6
  * Auth Client - HTTP client factory for authentication
3
7
  *
4
8
  * Creates a thin HTTP wrapper that can be injected into operations.
5
9
  */
6
10
 
7
- import { getPackageVersion } from '../utils/package-info.js';
8
- import { buildAuthUserAgent, buildRequestHeaders, parseAuthError, parseAuthenticatedError } from './core.js';
9
-
10
11
  /**
11
12
  * Parse response body based on content type
12
13
  * @param {Response} response - Fetch response
@@ -0,0 +1,50 @@
1
+ import { AuthError, VizzlyError } from '../../shared/vizzly-error.js';
2
+
3
+ /**
4
+ * Parse error from API response
5
+ * @param {number} status - HTTP status code
6
+ * @param {Object|string} body - Response body (parsed JSON or text)
7
+ * @param {string} endpoint - API endpoint for context
8
+ * @returns {Error} Appropriate error type
9
+ */
10
+ export function parseAuthError(status, body, _endpoint) {
11
+ let errorText = '';
12
+ if (typeof body === 'object' && body !== null) {
13
+ errorText = body.error || body.message || '';
14
+ } else if (typeof body === 'string') {
15
+ errorText = body;
16
+ }
17
+ let retryAfter = Number(body?.retryAfter);
18
+ let context = {
19
+ status,
20
+ serverCode: body?.code,
21
+ retryAfter: retryAfter > 0 ? retryAfter : undefined
22
+ };
23
+ if (status === 401) {
24
+ return new AuthError(errorText || 'Invalid credentials. Please check your email/username and password.', context);
25
+ }
26
+ if (status === 429) {
27
+ return new VizzlyError('Too many login attempts. Please try again later.', 'RATE_LIMIT_ERROR', context);
28
+ }
29
+ return new VizzlyError(`Authentication request failed: ${status}${errorText ? ` - ${errorText}` : ''}`, 'AUTH_REQUEST_ERROR', context);
30
+ }
31
+
32
+ /**
33
+ * Parse error for authenticated requests (different error messages)
34
+ * @param {number} status - HTTP status code
35
+ * @param {Object|string} body - Response body
36
+ * @param {string} endpoint - API endpoint
37
+ * @returns {Error} Appropriate error type
38
+ */
39
+ export function parseAuthenticatedError(status, body, endpoint) {
40
+ let errorText = '';
41
+ if (typeof body === 'object' && body !== null) {
42
+ errorText = body.error || body.message || '';
43
+ } else if (typeof body === 'string') {
44
+ errorText = body;
45
+ }
46
+ if (status === 401) {
47
+ return new AuthError('Authentication token is invalid or expired. Please run "vizzly login" again.');
48
+ }
49
+ return new VizzlyError(`API request failed: ${status}${errorText ? ` - ${errorText}` : ''} (${endpoint})`, 'API_REQUEST_ERROR');
50
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Build Authorization header from access token
3
+ * @param {string|null} accessToken - Access token
4
+ * @returns {Object} Headers object with Authorization if token exists
5
+ */
6
+ export function buildAuthHeader(accessToken) {
7
+ if (!accessToken) return {};
8
+ return {
9
+ Authorization: `Bearer ${accessToken}`
10
+ };
11
+ }
12
+
13
+ /**
14
+ * Build User-Agent header for auth requests
15
+ * @param {string} version - CLI version
16
+ * @returns {string} User-Agent string
17
+ */
18
+ export function buildAuthUserAgent(version) {
19
+ return `vizzly-cli/${version} (auth)`;
20
+ }
21
+
22
+ /**
23
+ * Build complete headers for a request
24
+ * @param {Object} options - Header options
25
+ * @returns {Object} Complete headers object
26
+ */
27
+ export function buildRequestHeaders({
28
+ userAgent,
29
+ accessToken,
30
+ contentType,
31
+ extra = {}
32
+ }) {
33
+ return {
34
+ 'User-Agent': userAgent,
35
+ ...(accessToken ? buildAuthHeader(accessToken) : {}),
36
+ ...(contentType ? {
37
+ 'Content-Type': contentType
38
+ } : {}),
39
+ ...extra
40
+ };
41
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Build token data for storage from API response
3
+ * @param {Object} response - API response with tokens
4
+ * @param {Object|null} existingUser - Existing user data to preserve
5
+ * @returns {Object} Token data for storage
6
+ */
7
+ export function buildTokenData(response, existingUser = null) {
8
+ return {
9
+ accessToken: response.accessToken,
10
+ refreshToken: response.refreshToken,
11
+ expiresAt: response.expiresAt,
12
+ user: response.user || existingUser
13
+ };
14
+ }
15
+ export function normalizeDeviceFlowResponse(deviceFlow) {
16
+ let verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
17
+ let userCode = deviceFlow.user_code || deviceFlow.userCode;
18
+ let deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
19
+ if (!verificationUri || !userCode || !deviceCode) {
20
+ throw new Error('Invalid device flow response from server');
21
+ }
22
+ return {
23
+ verificationUri,
24
+ userCode,
25
+ deviceCode,
26
+ urlWithCode: deviceFlow.verification_uri_complete || deviceFlow.verificationUriComplete || `${verificationUri}?code=${encodeURIComponent(userCode)}`
27
+ };
28
+ }
29
+ export function buildTokens(tokenData, now = Date.now()) {
30
+ let tokensData = tokenData.tokens || tokenData;
31
+ let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
32
+ let tokenExpiresAt = tokenExpiresIn ? new Date(now + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
33
+ return {
34
+ accessToken: tokensData.accessToken || tokensData.access_token,
35
+ refreshToken: tokensData.refreshToken || tokensData.refresh_token,
36
+ expiresAt: tokenExpiresAt,
37
+ user: tokenData.user,
38
+ organizations: tokenData.organizations
39
+ };
40
+ }
41
+
42
+ /** Map protocol codes without depending on the server's human-readable text. */
43
+ export function deviceAuthorizationStatus(error) {
44
+ return {
45
+ AUTHORIZATION_PENDING: 'pending',
46
+ ACCESS_DENIED: 'denied',
47
+ EXPIRED_TOKEN: 'expired',
48
+ INVALID_DEVICE_CODE: 'invalid'
49
+ }[error.context?.serverCode];
50
+ }
@@ -1,13 +1,13 @@
1
+ import { parseJsonBody } from '../../adapters/http/middleware/json-parser.js';
2
+ import { sendError, sendJson, sendServiceUnavailable, sendSuccess } from '../../adapters/http/middleware/response.js';
3
+ import * as output from '../../shared/output.js';
4
+ import { buildTokens, normalizeDeviceFlowResponse } from './auth.normalizers.js';
5
+
1
6
  /**
2
7
  * Auth Router
3
8
  * Handles authentication endpoints (device flow login, logout, status)
4
9
  */
5
10
 
6
- import { buildTokens, normalizeDeviceFlowResponse } from '../../auth/core.js';
7
- import * as output from '../../utils/output.js';
8
- import { parseJsonBody } from '../middleware/json-parser.js';
9
- import { sendError, sendJson, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
10
-
11
11
  /**
12
12
  * Create auth router
13
13
  * @param {Object} context - Router context
@@ -27,10 +27,10 @@ export function createAuthRouter({
27
27
  // Get auth status and user info
28
28
  if (req.method === 'GET' && pathname === '/api/auth/status') {
29
29
  try {
30
- const isAuthenticated = await authService.isAuthenticated();
30
+ let isAuthenticated = await authService.isAuthenticated();
31
31
  let user = null;
32
32
  if (isAuthenticated) {
33
- const whoami = await authService.whoami();
33
+ let whoami = await authService.whoami();
34
34
  user = whoami.user;
35
35
  }
36
36
  sendSuccess(res, {
@@ -72,8 +72,8 @@ export function createAuthRouter({
72
72
  // Poll device authorization status
73
73
  if (req.method === 'POST' && pathname === '/api/auth/poll') {
74
74
  try {
75
- const body = await parseJsonBody(req);
76
- const {
75
+ let body = await parseJsonBody(req);
76
+ let {
77
77
  deviceCode
78
78
  } = body;
79
79
  if (!deviceCode) {
@@ -0,0 +1,19 @@
1
+ export function resolveAuthorizedTokenData(pollResponse) {
2
+ let accessToken = pollResponse.tokens?.accessToken || pollResponse.tokens?.access_token;
3
+ if (accessToken) {
4
+ return pollResponse;
5
+ }
6
+ if (pollResponse.status === 'pending') {
7
+ throw new Error('Authorization not complete yet. Please complete the authorization in your browser and try running "vizzly login" again.');
8
+ }
9
+ if (pollResponse.status === 'expired') {
10
+ throw new Error('Device code expired. Please try logging in again.');
11
+ }
12
+ if (pollResponse.status === 'invalid') {
13
+ throw new Error('Device code is invalid or expired. Please try logging in again.');
14
+ }
15
+ if (pollResponse.status === 'denied') {
16
+ throw new Error('Authorization denied. Please try logging in again.');
17
+ }
18
+ throw new Error('Unexpected response from authorization server. Please try logging in again.');
19
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Build device poll request payload
3
+ * @param {string} deviceCode - Device code from initiate
4
+ * @returns {Object} Request payload
5
+ */
6
+ export function buildDevicePollPayload(deviceCode) {
7
+ return {
8
+ device_code: deviceCode
9
+ };
10
+ }
11
+
12
+ /**
13
+ * Build refresh token request payload
14
+ * @param {string} refreshToken - Refresh token
15
+ * @returns {Object} Request payload
16
+ */
17
+ export function buildRefreshPayload(refreshToken) {
18
+ return {
19
+ refreshToken
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Build logout request payload
25
+ * @param {string} refreshToken - Refresh token to revoke
26
+ * @returns {Object} Request payload
27
+ */
28
+ export function buildLogoutPayload(refreshToken) {
29
+ return {
30
+ refreshToken
31
+ };
32
+ }
@@ -1,3 +1,11 @@
1
+ import * as output from '../../shared/output.js';
2
+ import { getApiUrl } from '../config/environment.js';
3
+ import { buildTokenData, deviceAuthorizationStatus } from './auth.normalizers.js';
4
+ import { buildDevicePollPayload, buildLogoutPayload, buildRefreshPayload } from './auth.serializers.js';
5
+ import { validateTokens } from './auth.validations.js';
6
+ import { createAuthClient } from './auth-client.js';
7
+ import { createTokenStore } from './token-store.js';
8
+
1
9
  /**
2
10
  * Auth Operations - Authentication operations with dependency injection
3
11
  *
@@ -8,12 +16,114 @@
8
16
  * This makes them trivially testable without mocking modules.
9
17
  */
10
18
 
11
- import * as output from '../utils/output.js';
12
- import { buildDevicePollPayload, buildLogoutPayload, buildRefreshPayload, buildTokenData, deviceAuthorizationStatus, validateTokens } from './core.js';
19
+ /**
20
+ * Auth Service
21
+ * Wraps auth operations for use by the HTTP server
22
+ *
23
+ * Provides the interface expected by src/domains/auth/auth.routes.js:
24
+ * - isAuthenticated() - Returns boolean, false if no tokens or API call fails
25
+ * - whoami() - Throws if not authenticated or tokens invalid
26
+ * - initiateDeviceFlow() - Throws on API error
27
+ * - pollDeviceAuthorization(deviceCode) - Returns pending status or tokens, throws on error
28
+ * - completeDeviceFlow(tokens) - Saves tokens to global config
29
+ * - logout() - Clears local tokens, may warn if server revocation fails
30
+ * - authenticatedRequest(endpoint, options) - Throws 'Not authenticated' if no tokens
31
+ *
32
+ * Error handling:
33
+ * - isAuthenticated() never throws, returns false on any error
34
+ * - whoami() throws if tokens are missing/invalid (caller should check isAuthenticated first)
35
+ * - authenticatedRequest() throws 'Not authenticated' if no access token
36
+ * - Device flow methods throw on API errors (network, server errors)
37
+ */
13
38
 
14
- // ============================================================================
15
- // Device Flow Operations
16
- // ============================================================================
39
+ /**
40
+ * Create an auth service instance
41
+ * @param {Object} [options]
42
+ * @param {string} [options.apiUrl] - API base URL (defaults to VIZZLY_API_URL or https://app.vizzly.dev)
43
+ * @param {Object} [options.httpClient] - Injectable HTTP client (for testing)
44
+ * @param {Object} [options.tokenStore] - Injectable token store (for testing)
45
+ * @returns {Object} Auth service
46
+ */
47
+ export function createAuthService(options = {}) {
48
+ let apiUrl = options.apiUrl || getApiUrl();
49
+
50
+ // Create HTTP client for API requests (uses auth client for proper auth handling)
51
+ // Allow injection for testing
52
+ let httpClient = options.httpClient || createAuthClient({
53
+ baseUrl: apiUrl
54
+ });
55
+
56
+ // Create token store adapter for global config
57
+ // Allow injection for testing
58
+ let tokenStore = options.tokenStore || createTokenStore();
59
+ return {
60
+ /**
61
+ * Check if user is authenticated
62
+ * @returns {Promise<boolean>}
63
+ */
64
+ async isAuthenticated() {
65
+ return isAuthenticated(httpClient, tokenStore);
66
+ },
67
+ /**
68
+ * Get current user information
69
+ * @returns {Promise<Object>} User and organization data
70
+ */
71
+ async whoami() {
72
+ return whoami(httpClient, tokenStore);
73
+ },
74
+ /**
75
+ * Initiate OAuth device flow
76
+ * @returns {Promise<Object>} Device code info
77
+ */
78
+ async initiateDeviceFlow() {
79
+ return initiateDeviceFlow(httpClient);
80
+ },
81
+ /**
82
+ * Poll for device authorization
83
+ * @param {string} deviceCode
84
+ * @returns {Promise<Object>} Token data or pending status
85
+ */
86
+ async pollDeviceAuthorization(deviceCode) {
87
+ return pollDeviceAuthorization(httpClient, deviceCode);
88
+ },
89
+ /**
90
+ * Complete device flow and save tokens
91
+ * @param {Object} tokens - Token data
92
+ * @returns {Promise<Object>}
93
+ */
94
+ async completeDeviceFlow(tokens) {
95
+ return completeDeviceFlow(tokenStore, tokens);
96
+ },
97
+ /**
98
+ * Logout and revoke tokens
99
+ * @returns {Promise<void>}
100
+ */
101
+ async logout() {
102
+ return logout(httpClient, tokenStore);
103
+ },
104
+ /**
105
+ * Refresh access token
106
+ * @returns {Promise<Object>} New tokens
107
+ */
108
+ async refresh() {
109
+ return refresh(httpClient, tokenStore);
110
+ },
111
+ /**
112
+ * Make an authenticated request to the API
113
+ * Used by cloud-proxy router for proxying requests
114
+ * @param {string} endpoint - API endpoint
115
+ * @param {Object} options - Fetch options
116
+ * @returns {Promise<Object>} Response data
117
+ */
118
+ async authenticatedRequest(endpoint, options = {}) {
119
+ let auth = await tokenStore.getTokens();
120
+ if (!auth?.accessToken) {
121
+ throw new Error('Not authenticated');
122
+ }
123
+ return httpClient.authenticatedRequest(endpoint, auth.accessToken, options);
124
+ }
125
+ };
126
+ }
17
127
 
18
128
  /**
19
129
  * Initiate OAuth device flow
@@ -65,10 +175,6 @@ export async function completeDeviceFlow(tokenStore, tokenData) {
65
175
  return tokenData;
66
176
  }
67
177
 
68
- // ============================================================================
69
- // Token Operations
70
- // ============================================================================
71
-
72
178
  /**
73
179
  * Refresh access token using refresh token
74
180
  * @param {Object} httpClient - HTTP client
@@ -94,10 +200,6 @@ export async function refresh(httpClient, tokenStore) {
94
200
  return response;
95
201
  }
96
202
 
97
- // ============================================================================
98
- // Logout Operations
99
- // ============================================================================
100
-
101
203
  /**
102
204
  * Logout and revoke tokens
103
205
  * @param {Object} httpClient - HTTP client
@@ -123,10 +225,6 @@ export async function logout(httpClient, tokenStore) {
123
225
  await tokenStore.clearTokens();
124
226
  }
125
227
 
126
- // ============================================================================
127
- // User Operations
128
- // ============================================================================
129
-
130
228
  /**
131
229
  * Get current user information
132
230
  * @param {Object} httpClient - HTTP client
@@ -0,0 +1,21 @@
1
+ import { AuthError } from '../../shared/vizzly-error.js';
2
+
3
+ /**
4
+ * Validate that tokens exist and have required fields
5
+ * @param {Object|null} auth - Auth tokens object
6
+ * @param {string} requiredField - Field that must exist ('accessToken' or 'refreshToken')
7
+ * @returns {{ valid: boolean, error: Error|null }}
8
+ */
9
+ export function validateTokens(auth, requiredField = 'accessToken') {
10
+ if (!auth?.[requiredField]) {
11
+ let message = requiredField === 'refreshToken' ? 'No refresh token found. Please run "vizzly login" first.' : 'No authentication token found. Please run "vizzly login" first.';
12
+ return {
13
+ valid: false,
14
+ error: new AuthError(message)
15
+ };
16
+ }
17
+ return {
18
+ valid: true,
19
+ error: null
20
+ };
21
+ }
@@ -5,10 +5,10 @@
5
5
  * The client handles authentication, token refresh, and error handling.
6
6
  */
7
7
 
8
- import { AuthError, VizzlyError } from '../errors/vizzly-error.js';
9
- import { getAuthTokens, saveAuthTokens } from '../utils/global-config.js';
10
- import { getPackageVersion } from '../utils/package-info.js';
11
- import { buildApiUrl, buildRequestHeaders, buildUserAgent, extractErrorBody, isAuthError, parseApiError, shouldRetryWithRefresh } from './core.js';
8
+ import { buildApiUrl, buildRequestHeaders, buildUserAgent, extractErrorBody, isAuthError, parseApiError } from '../../shared/cloud-protocol.js';
9
+ import { getPackageVersion } from '../../shared/package-info.js';
10
+ import { AuthError, VizzlyError } from '../../shared/vizzly-error.js';
11
+ import { getAuthTokens, saveAuthTokens } from './token-store.js';
12
12
 
13
13
  /**
14
14
  * Default API URL
@@ -85,8 +85,9 @@ export function createApiClient(options = {}) {
85
85
  let errorBody = await extractErrorBody(response);
86
86
 
87
87
  // Handle 401 with token refresh
88
- if (retryAuthentication && shouldRetryWithRefresh(response.status, isRetry, !isProjectToken(token) && (await hasRefreshToken()))) {
89
- let refreshed = await attemptTokenRefresh();
88
+ if (retryAuthentication && response.status === 401 && !isRetry && !isProjectToken(token)) {
89
+ let auth = await getAuthTokens();
90
+ let refreshed = auth?.accessToken === token && auth.refreshToken ? await attemptTokenRefresh(auth) : null;
90
91
  if (refreshed) {
91
92
  token = refreshed;
92
93
  return request(endpoint, fetchOptions, true);
@@ -95,7 +96,9 @@ export function createApiClient(options = {}) {
95
96
 
96
97
  // Auth error
97
98
  if (isAuthError(response.status)) {
98
- throw new AuthError('Invalid or expired API token. Run "vizzly project link <org>/<project>" or set VIZZLY_TOKEN.');
99
+ throw new AuthError('Invalid or expired API token. Run "vizzly project link <org>/<project>" or set VIZZLY_TOKEN.', {
100
+ status: response.status
101
+ });
99
102
  }
100
103
 
101
104
  // Other errors
@@ -114,21 +117,11 @@ export function createApiClient(options = {}) {
114
117
  return response.json();
115
118
  }
116
119
 
117
- /**
118
- * Check if refresh token is available
119
- */
120
- async function hasRefreshToken() {
121
- let auth = await getAuthTokens();
122
- return !!auth?.refreshToken;
123
- }
124
-
125
120
  /**
126
121
  * Attempt to refresh the access token
127
122
  * @returns {Promise<string|null>} New token or null if refresh failed
128
123
  */
129
- async function attemptTokenRefresh() {
130
- let auth = await getAuthTokens();
131
- if (!auth?.refreshToken) return null;
124
+ async function attemptTokenRefresh(auth) {
132
125
  try {
133
126
  let refreshUrl = buildApiUrl(baseUrl, '/api/auth/cli/refresh');
134
127
  let response = await fetch(refreshUrl, {
@@ -1,16 +1,19 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { openBrowser } from '../../shared/browser.js';
3
+ import * as output from '../../shared/output.js';
4
+ import { getApiUrl } from '../config/environment.js';
5
+ import { buildTokens, normalizeDeviceFlowResponse } from './auth.normalizers.js';
6
+ import { resolveAuthorizedTokenData } from './auth.rules.js';
7
+ import { completeDeviceFlow, initiateDeviceFlow, pollDeviceAuthorization } from './auth.service.js';
8
+ import { createAuthClient } from './auth-client.js';
9
+ import { waitForLoginPoll } from './login-wait.js';
10
+ import { createTokenStore } from './token-store.js';
11
+
1
12
  /**
2
13
  * Login command implementation
3
14
  * Authenticates user via OAuth device flow
4
15
  */
5
16
 
6
- import { setTimeout as delay } from 'node:timers/promises';
7
- import { buildTokens, normalizeDeviceFlowResponse, resolveAuthorizedTokenData } from '../auth/core.js';
8
- import { completeDeviceFlow, createAuthClient, createTokenStore, initiateDeviceFlow, pollDeviceAuthorization } from '../auth/index.js';
9
- import { openBrowser } from '../utils/browser.js';
10
- import { getApiUrl } from '../utils/environment-config.js';
11
- import { waitForLoginPoll } from '../utils/login-poll-wait.js';
12
- import * as output from '../utils/output.js';
13
-
14
17
  /**
15
18
  * Login command implementation using OAuth device flow
16
19
  * @param {Object} options - Command options
@@ -1,12 +1,14 @@
1
+ import * as defaultOutput from '../../shared/output.js';
2
+ import { getApiUrl as defaultGetApiUrl } from '../config/environment.js';
3
+ import { logout as defaultLogout } from './auth.service.js';
4
+ import { createAuthClient as defaultCreateAuthClient } from './auth-client.js';
5
+ import { createTokenStore as defaultCreateTokenStore, getAuthTokens as defaultGetAuthTokens } from './token-store.js';
6
+
1
7
  /**
2
8
  * Logout command implementation
3
9
  * Clears stored authentication tokens
4
10
  */
5
11
 
6
- import { createAuthClient as defaultCreateAuthClient, createTokenStore as defaultCreateTokenStore, getAuthTokens as defaultGetAuthTokens, logout as defaultLogout } from '../auth/index.js';
7
- import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
8
- import * as defaultOutput from '../utils/output.js';
9
-
10
12
  /**
11
13
  * Logout command implementation
12
14
  * @param {Object} options - Command options
@@ -0,0 +1,81 @@
1
+ import { loadGlobalConfig, saveGlobalConfig } from '../../shared/global-config.js';
2
+
3
+ /**
4
+ * Get authentication tokens from global config
5
+ * @returns {Promise<Object|null>} Token object with accessToken, refreshToken, expiresAt, user, or null if not found
6
+ */
7
+ export async function getAuthTokens() {
8
+ let config = await loadGlobalConfig();
9
+ if (!config.auth?.accessToken) {
10
+ return null;
11
+ }
12
+ return config.auth;
13
+ }
14
+
15
+ /**
16
+ * Save authentication tokens to global config
17
+ * @param {Object} auth - Auth object with accessToken, refreshToken, expiresAt, user
18
+ * @returns {Promise<void>}
19
+ */
20
+ export async function saveAuthTokens(auth) {
21
+ let config = await loadGlobalConfig();
22
+ config.auth = {
23
+ accessToken: auth.accessToken,
24
+ refreshToken: auth.refreshToken,
25
+ expiresAt: auth.expiresAt,
26
+ user: auth.user
27
+ };
28
+ await saveGlobalConfig(config);
29
+ }
30
+
31
+ /**
32
+ * Clear authentication tokens from global config
33
+ * @returns {Promise<void>}
34
+ */
35
+ export async function clearAuthTokens() {
36
+ let config = await loadGlobalConfig();
37
+ delete config.auth;
38
+ await saveGlobalConfig(config);
39
+ }
40
+ function hasValidAccessToken(auth, now = Date.now()) {
41
+ if (!auth?.accessToken) {
42
+ return false;
43
+ }
44
+ if (!auth.expiresAt) {
45
+ return true;
46
+ }
47
+ let expiresAt = new Date(auth.expiresAt).getTime();
48
+ // Treat tokens expiring within five minutes as unavailable.
49
+ let bufferMs = 5 * 60 * 1000;
50
+ return now < expiresAt - bufferMs;
51
+ }
52
+
53
+ /**
54
+ * Check if authentication tokens exist and are not expired
55
+ * @returns {Promise<boolean>} True if valid tokens exist
56
+ */
57
+ export async function hasValidTokens() {
58
+ let auth = await getAuthTokens();
59
+ return hasValidAccessToken(auth);
60
+ }
61
+
62
+ /**
63
+ * Get the access token from global config if valid and not expired
64
+ * @returns {Promise<string|null>} Access token or null
65
+ */
66
+ export async function getAccessToken() {
67
+ let auth = await getAuthTokens();
68
+ return hasValidAccessToken(auth) ? auth.accessToken : null;
69
+ }
70
+
71
+ /**
72
+ * Create a token store adapter from global-config functions
73
+ * Used by auth operations that need tokenStore parameter
74
+ */
75
+ export function createTokenStore() {
76
+ return {
77
+ getTokens: getAuthTokens,
78
+ saveTokens: saveAuthTokens,
79
+ clearTokens: clearAuthTokens
80
+ };
81
+ }
@@ -1,11 +1,14 @@
1
+ import * as defaultOutput from '../../shared/output.js';
2
+ import { getApiUrl as defaultGetApiUrl } from '../config/environment.js';
3
+ import { whoami as defaultWhoami } from './auth.service.js';
4
+ import { createAuthClient as defaultCreateAuthClient } from './auth-client.js';
5
+ import { createTokenStore as defaultCreateTokenStore, getAuthTokens as defaultGetAuthTokens } from './token-store.js';
6
+
1
7
  /**
2
8
  * Whoami command implementation
3
9
  * Shows current user and authentication status
4
10
  */
5
11
 
6
- import { createAuthClient as defaultCreateAuthClient, createTokenStore as defaultCreateTokenStore, getAuthTokens as defaultGetAuthTokens, whoami as defaultWhoami } from '../auth/index.js';
7
- import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
8
- import * as defaultOutput from '../utils/output.js';
9
12
  export function getTokenExpiryStatus(expiresAt, now = new Date()) {
10
13
  let expiresDate = new Date(expiresAt);
11
14
  let msUntilExpiry = expiresDate.getTime() - now.getTime();