@vizzly-testing/cli 0.20.0 → 0.20.1-beta.1

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 (84) hide show
  1. package/dist/api/client.js +134 -0
  2. package/dist/api/core.js +341 -0
  3. package/dist/api/endpoints.js +314 -0
  4. package/dist/api/index.js +19 -0
  5. package/dist/auth/client.js +91 -0
  6. package/dist/auth/core.js +176 -0
  7. package/dist/auth/index.js +30 -0
  8. package/dist/auth/operations.js +148 -0
  9. package/dist/cli.js +178 -3
  10. package/dist/client/index.js +144 -77
  11. package/dist/commands/doctor.js +121 -36
  12. package/dist/commands/finalize.js +49 -18
  13. package/dist/commands/init.js +13 -18
  14. package/dist/commands/login.js +49 -55
  15. package/dist/commands/logout.js +17 -9
  16. package/dist/commands/project.js +100 -71
  17. package/dist/commands/run.js +189 -95
  18. package/dist/commands/status.js +101 -66
  19. package/dist/commands/tdd-daemon.js +61 -32
  20. package/dist/commands/tdd.js +104 -98
  21. package/dist/commands/upload.js +78 -34
  22. package/dist/commands/whoami.js +44 -42
  23. package/dist/config/core.js +438 -0
  24. package/dist/config/index.js +13 -0
  25. package/dist/config/operations.js +327 -0
  26. package/dist/index.js +1 -1
  27. package/dist/project/core.js +295 -0
  28. package/dist/project/index.js +13 -0
  29. package/dist/project/operations.js +393 -0
  30. package/dist/reporter/reporter-bundle.css +1 -1
  31. package/dist/reporter/reporter-bundle.iife.js +16 -16
  32. package/dist/screenshot-server/core.js +157 -0
  33. package/dist/screenshot-server/index.js +11 -0
  34. package/dist/screenshot-server/operations.js +183 -0
  35. package/dist/sdk/index.js +3 -2
  36. package/dist/server/handlers/api-handler.js +14 -5
  37. package/dist/server/handlers/tdd-handler.js +191 -53
  38. package/dist/server/http-server.js +9 -3
  39. package/dist/server/routers/baseline.js +58 -0
  40. package/dist/server/routers/dashboard.js +10 -6
  41. package/dist/server/routers/screenshot.js +32 -0
  42. package/dist/server-manager/core.js +186 -0
  43. package/dist/server-manager/index.js +81 -0
  44. package/dist/server-manager/operations.js +209 -0
  45. package/dist/services/build-manager.js +2 -69
  46. package/dist/services/index.js +21 -48
  47. package/dist/services/screenshot-server.js +40 -74
  48. package/dist/services/server-manager.js +45 -80
  49. package/dist/services/test-runner.js +90 -250
  50. package/dist/services/uploader.js +56 -358
  51. package/dist/tdd/core/hotspot-coverage.js +112 -0
  52. package/dist/tdd/core/signature.js +101 -0
  53. package/dist/tdd/index.js +19 -0
  54. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  55. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  56. package/dist/tdd/services/baseline-downloader.js +151 -0
  57. package/dist/tdd/services/baseline-manager.js +166 -0
  58. package/dist/tdd/services/comparison-service.js +230 -0
  59. package/dist/tdd/services/hotspot-service.js +71 -0
  60. package/dist/tdd/services/result-service.js +123 -0
  61. package/dist/tdd/tdd-service.js +1145 -0
  62. package/dist/test-runner/core.js +255 -0
  63. package/dist/test-runner/index.js +13 -0
  64. package/dist/test-runner/operations.js +483 -0
  65. package/dist/types/client.d.ts +25 -2
  66. package/dist/uploader/core.js +396 -0
  67. package/dist/uploader/index.js +11 -0
  68. package/dist/uploader/operations.js +412 -0
  69. package/dist/utils/colors.js +187 -39
  70. package/dist/utils/config-loader.js +3 -6
  71. package/dist/utils/context.js +228 -0
  72. package/dist/utils/output.js +449 -14
  73. package/docs/api-reference.md +173 -8
  74. package/docs/tui-elements.md +560 -0
  75. package/package.json +13 -13
  76. package/dist/services/api-service.js +0 -412
  77. package/dist/services/auth-service.js +0 -226
  78. package/dist/services/config-service.js +0 -369
  79. package/dist/services/html-report-generator.js +0 -455
  80. package/dist/services/project-service.js +0 -326
  81. package/dist/services/report-generator/report.css +0 -411
  82. package/dist/services/report-generator/viewer.js +0 -102
  83. package/dist/services/static-report-generator.js +0 -207
  84. package/dist/services/tdd-service.js +0 -1437
@@ -0,0 +1,314 @@
1
+ /**
2
+ * API Endpoints - Functions for each API operation
3
+ *
4
+ * Each function takes a client as the first parameter and returns the API result.
5
+ * This keeps the functions pure (no hidden state) and easily testable.
6
+ */
7
+
8
+ import { VizzlyError } from '../errors/vizzly-error.js';
9
+ import * as output from '../utils/output.js';
10
+ import { buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildScreenshotCheckObject, buildScreenshotPayload, buildShaCheckPayload, computeSha256, findScreenshotBySha, shaExists } from './core.js';
11
+
12
+ // ============================================================================
13
+ // Build Endpoints
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Get build information
18
+ * @param {Object} client - API client
19
+ * @param {string} buildId - Build ID
20
+ * @param {string|null} include - Optional include parameter (e.g., 'screenshots')
21
+ * @returns {Promise<Object>} Build data
22
+ */
23
+ export async function getBuild(client, buildId, include = null) {
24
+ let endpoint = `/api/sdk/builds/${buildId}`;
25
+ if (include) {
26
+ endpoint = buildEndpointWithParams(endpoint, {
27
+ include
28
+ });
29
+ }
30
+ return client.request(endpoint);
31
+ }
32
+
33
+ /**
34
+ * Get builds for a project
35
+ * @param {Object} client - API client
36
+ * @param {Object} filters - Filter options
37
+ * @returns {Promise<Array>} List of builds
38
+ */
39
+ export async function getBuilds(client, filters = {}) {
40
+ let query = buildQueryParams(filters);
41
+ let endpoint = `/api/sdk/builds${query ? `?${query}` : ''}`;
42
+ return client.request(endpoint);
43
+ }
44
+
45
+ /**
46
+ * Create a new build
47
+ * @param {Object} client - API client
48
+ * @param {Object} metadata - Build metadata
49
+ * @returns {Promise<Object>} Created build data
50
+ */
51
+ export async function createBuild(client, metadata) {
52
+ let payload = buildBuildPayload(metadata);
53
+ return client.request('/api/sdk/builds', {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json'
57
+ },
58
+ body: JSON.stringify({
59
+ build: payload
60
+ })
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Update build status
66
+ * @param {Object} client - API client
67
+ * @param {string} buildId - Build ID
68
+ * @param {string} status - Build status (pending|running|completed|failed)
69
+ * @param {number|null} executionTimeMs - Execution time in milliseconds
70
+ * @returns {Promise<Object>} Updated build data
71
+ */
72
+ export async function updateBuildStatus(client, buildId, status, executionTimeMs = null) {
73
+ let body = {
74
+ status
75
+ };
76
+ if (executionTimeMs != null) {
77
+ body.executionTimeMs = executionTimeMs;
78
+ }
79
+ return client.request(`/api/sdk/builds/${buildId}/status`, {
80
+ method: 'PUT',
81
+ headers: {
82
+ 'Content-Type': 'application/json'
83
+ },
84
+ body: JSON.stringify(body)
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Finalize a build (convenience wrapper for updateBuildStatus)
90
+ * @param {Object} client - API client
91
+ * @param {string} buildId - Build ID
92
+ * @param {boolean} success - Whether the build succeeded
93
+ * @param {number|null} executionTimeMs - Execution time in milliseconds
94
+ * @returns {Promise<Object>} Finalized build data
95
+ */
96
+ export async function finalizeBuild(client, buildId, success = true, executionTimeMs = null) {
97
+ let status = success ? 'completed' : 'failed';
98
+ return updateBuildStatus(client, buildId, status, executionTimeMs);
99
+ }
100
+
101
+ /**
102
+ * Get TDD baselines for a build
103
+ * @param {Object} client - API client
104
+ * @param {string} buildId - Build ID
105
+ * @returns {Promise<Object>} { build, screenshots, signatureProperties }
106
+ */
107
+ export async function getTddBaselines(client, buildId) {
108
+ return client.request(`/api/sdk/builds/${buildId}/tdd-baselines`);
109
+ }
110
+
111
+ // ============================================================================
112
+ // Screenshot Endpoints
113
+ // ============================================================================
114
+
115
+ /**
116
+ * Check if SHAs already exist on the server
117
+ * @param {Object} client - API client
118
+ * @param {Array} screenshots - Screenshots to check (objects with sha256, or string SHAs)
119
+ * @param {string} buildId - Build ID for screenshot record creation
120
+ * @returns {Promise<Object>} { existing, missing, screenshots }
121
+ */
122
+ export async function checkShas(client, screenshots, buildId) {
123
+ try {
124
+ let payload = buildShaCheckPayload(screenshots, buildId);
125
+ return await client.request('/api/sdk/check-shas', {
126
+ method: 'POST',
127
+ headers: {
128
+ 'Content-Type': 'application/json'
129
+ },
130
+ body: JSON.stringify(payload)
131
+ });
132
+ } catch (error) {
133
+ // Continue without deduplication on error
134
+ output.debug('sha-check', 'failed, continuing without deduplication', {
135
+ error: error.message
136
+ });
137
+
138
+ // Extract SHAs for fallback response
139
+ let shaList = Array.isArray(screenshots) && screenshots.length > 0 && typeof screenshots[0] === 'object' ? screenshots.map(s => s.sha256) : screenshots;
140
+ return {
141
+ existing: [],
142
+ missing: shaList,
143
+ screenshots: []
144
+ };
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Upload a screenshot with SHA deduplication
150
+ * @param {Object} client - API client
151
+ * @param {string} buildId - Build ID
152
+ * @param {string} name - Screenshot name
153
+ * @param {Buffer} buffer - Screenshot data
154
+ * @param {Object} metadata - Additional metadata
155
+ * @param {boolean} skipDedup - Skip SHA deduplication (uploadAll mode)
156
+ * @returns {Promise<Object>} Upload result
157
+ */
158
+ export async function uploadScreenshot(client, buildId, name, buffer, metadata = {}, skipDedup = false) {
159
+ // Skip SHA deduplication if requested
160
+ if (skipDedup) {
161
+ let payload = buildScreenshotPayload(name, buffer, metadata);
162
+ return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
163
+ method: 'POST',
164
+ headers: {
165
+ 'Content-Type': 'application/json'
166
+ },
167
+ body: JSON.stringify(payload)
168
+ });
169
+ }
170
+
171
+ // Normal flow with SHA deduplication
172
+ let sha256 = computeSha256(buffer);
173
+ let checkObj = buildScreenshotCheckObject(sha256, name, metadata);
174
+ let checkResult = await checkShas(client, [checkObj], buildId);
175
+ if (shaExists(checkResult, sha256)) {
176
+ // File already exists, screenshot record was automatically created
177
+ let screenshot = findScreenshotBySha(checkResult, sha256);
178
+ return {
179
+ message: 'Screenshot already exists, skipped upload',
180
+ sha256,
181
+ skipped: true,
182
+ screenshot,
183
+ fromExisting: true
184
+ };
185
+ }
186
+
187
+ // File doesn't exist, proceed with upload
188
+ let payload = buildScreenshotPayload(name, buffer, metadata, sha256);
189
+ return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
190
+ method: 'POST',
191
+ headers: {
192
+ 'Content-Type': 'application/json'
193
+ },
194
+ body: JSON.stringify(payload)
195
+ });
196
+ }
197
+
198
+ // ============================================================================
199
+ // Comparison Endpoints
200
+ // ============================================================================
201
+
202
+ /**
203
+ * Get comparison information
204
+ * @param {Object} client - API client
205
+ * @param {string} comparisonId - Comparison ID
206
+ * @returns {Promise<Object>} Comparison data
207
+ */
208
+ export async function getComparison(client, comparisonId) {
209
+ let response = await client.request(`/api/sdk/comparisons/${comparisonId}`);
210
+ return response.comparison;
211
+ }
212
+
213
+ /**
214
+ * Search for comparisons by name
215
+ * @param {Object} client - API client
216
+ * @param {string} name - Screenshot name to search for
217
+ * @param {Object} filters - Optional filters (branch, limit, offset)
218
+ * @returns {Promise<Object>} Search results with comparisons and pagination
219
+ */
220
+ export async function searchComparisons(client, name, filters = {}) {
221
+ if (!name || typeof name !== 'string') {
222
+ throw new VizzlyError('name is required and must be a non-empty string');
223
+ }
224
+ let {
225
+ branch,
226
+ limit = 50,
227
+ offset = 0
228
+ } = filters;
229
+ let params = {
230
+ name,
231
+ limit: String(limit),
232
+ offset: String(offset)
233
+ };
234
+ if (branch) params.branch = branch;
235
+ let endpoint = buildEndpointWithParams('/api/sdk/comparisons/search', params);
236
+ return client.request(endpoint);
237
+ }
238
+
239
+ // ============================================================================
240
+ // Hotspot Endpoints
241
+ // ============================================================================
242
+
243
+ /**
244
+ * Get hotspot analysis for a single screenshot
245
+ * @param {Object} client - API client
246
+ * @param {string} screenshotName - Screenshot name
247
+ * @param {Object} options - Optional settings
248
+ * @returns {Promise<Object>} Hotspot analysis data
249
+ */
250
+ export async function getScreenshotHotspots(client, screenshotName, options = {}) {
251
+ let {
252
+ windowSize = 20
253
+ } = options;
254
+ let encodedName = encodeURIComponent(screenshotName);
255
+ let endpoint = buildEndpointWithParams(`/api/sdk/screenshots/${encodedName}/hotspots`, {
256
+ windowSize: String(windowSize)
257
+ });
258
+ return client.request(endpoint);
259
+ }
260
+
261
+ /**
262
+ * Batch get hotspot analysis for multiple screenshots
263
+ * @param {Object} client - API client
264
+ * @param {string[]} screenshotNames - Array of screenshot names
265
+ * @param {Object} options - Optional settings
266
+ * @returns {Promise<Object>} Hotspots keyed by screenshot name
267
+ */
268
+ export async function getBatchHotspots(client, screenshotNames, options = {}) {
269
+ let {
270
+ windowSize = 20
271
+ } = options;
272
+ return client.request('/api/sdk/screenshots/hotspots', {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Content-Type': 'application/json'
276
+ },
277
+ body: JSON.stringify({
278
+ screenshot_names: screenshotNames,
279
+ windowSize
280
+ })
281
+ });
282
+ }
283
+
284
+ // ============================================================================
285
+ // Auth/Token Endpoints
286
+ // ============================================================================
287
+
288
+ /**
289
+ * Get token context (organization and project info)
290
+ * @param {Object} client - API client
291
+ * @returns {Promise<Object>} Token context data
292
+ */
293
+ export async function getTokenContext(client) {
294
+ return client.request('/api/sdk/token/context');
295
+ }
296
+
297
+ // ============================================================================
298
+ // Parallel Build Endpoints
299
+ // ============================================================================
300
+
301
+ /**
302
+ * Finalize a parallel build
303
+ * @param {Object} client - API client
304
+ * @param {string} parallelId - Parallel ID to finalize
305
+ * @returns {Promise<Object>} Finalization result
306
+ */
307
+ export async function finalizeParallelBuild(client, parallelId) {
308
+ return client.request(`/api/sdk/parallel/${parallelId}/finalize`, {
309
+ method: 'POST',
310
+ headers: {
311
+ 'Content-Type': 'application/json'
312
+ }
313
+ });
314
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Vizzly API Module
3
+ *
4
+ * Functional API for interacting with the Vizzly platform.
5
+ *
6
+ * Usage:
7
+ * import { createApiClient, getBuild, createBuild } from '../api/index.js';
8
+ *
9
+ * let client = createApiClient({ token: 'xxx', command: 'run' });
10
+ * let build = await getBuild(client, buildId);
11
+ */
12
+
13
+ // Client factory
14
+ export { createApiClient, DEFAULT_API_URL } from './client.js';
15
+ // Core pure functions
16
+ export { buildApiUrl, buildAuthHeader, buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildRequestHeaders, buildScreenshotCheckObject, buildScreenshotPayload, buildShaCheckPayload, buildUserAgent, computeSha256, extractErrorBody, findScreenshotBySha, isAuthError, isRateLimited, parseApiError, partitionByShaExistence, shaExists, shouldRetryWithRefresh } from './core.js';
17
+
18
+ // Endpoint functions
19
+ export { checkShas, createBuild, finalizeBuild, finalizeParallelBuild, getBatchHotspots, getBuild, getBuilds, getComparison, getScreenshotHotspots, getTddBaselines, getTokenContext, searchComparisons, updateBuildStatus, uploadScreenshot } from './endpoints.js';
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Auth Client - HTTP client factory for authentication
3
+ *
4
+ * Creates a thin HTTP wrapper that can be injected into operations.
5
+ */
6
+
7
+ import { getPackageVersion } from '../utils/package-info.js';
8
+ import { buildAuthUserAgent, buildRequestHeaders, parseAuthError, parseAuthenticatedError } from './core.js';
9
+
10
+ /**
11
+ * Parse response body based on content type
12
+ * @param {Response} response - Fetch response
13
+ * @returns {Promise<Object|string>} Parsed body
14
+ */
15
+ async function parseResponseBody(response) {
16
+ try {
17
+ let contentType = response.headers.get('content-type');
18
+ if (contentType?.includes('application/json')) {
19
+ return await response.json();
20
+ }
21
+ return await response.text();
22
+ } catch {
23
+ return response.statusText || '';
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Create an auth HTTP client
29
+ * @param {Object} options - Client options
30
+ * @param {string} options.baseUrl - API base URL
31
+ * @param {string} [options.userAgent] - Custom user agent
32
+ * @returns {Object} Auth client with request methods
33
+ */
34
+ export function createAuthClient(options = {}) {
35
+ let {
36
+ baseUrl,
37
+ userAgent
38
+ } = options;
39
+ if (!userAgent) {
40
+ userAgent = buildAuthUserAgent(getPackageVersion());
41
+ }
42
+
43
+ /**
44
+ * Make an unauthenticated request
45
+ */
46
+ async function request(endpoint, fetchOptions = {}) {
47
+ let url = `${baseUrl}${endpoint}`;
48
+ let headers = buildRequestHeaders({
49
+ userAgent,
50
+ contentType: fetchOptions.headers?.['Content-Type'],
51
+ extra: fetchOptions.headers
52
+ });
53
+ let response = await fetch(url, {
54
+ ...fetchOptions,
55
+ headers
56
+ });
57
+ if (!response.ok) {
58
+ let body = await parseResponseBody(response);
59
+ throw parseAuthError(response.status, body, endpoint);
60
+ }
61
+ return response.json();
62
+ }
63
+
64
+ /**
65
+ * Make an authenticated request
66
+ */
67
+ async function authenticatedRequest(endpoint, accessToken, fetchOptions = {}) {
68
+ let url = `${baseUrl}${endpoint}`;
69
+ let headers = buildRequestHeaders({
70
+ userAgent,
71
+ accessToken,
72
+ contentType: fetchOptions.headers?.['Content-Type'],
73
+ extra: fetchOptions.headers
74
+ });
75
+ let response = await fetch(url, {
76
+ ...fetchOptions,
77
+ headers
78
+ });
79
+ if (!response.ok) {
80
+ let body = await parseResponseBody(response);
81
+ throw parseAuthenticatedError(response.status, body, endpoint);
82
+ }
83
+ return response.json();
84
+ }
85
+ return {
86
+ request,
87
+ authenticatedRequest,
88
+ getBaseUrl: () => baseUrl,
89
+ getUserAgent: () => userAgent
90
+ };
91
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Auth Core - Pure functions for authentication logic
3
+ *
4
+ * No I/O, no side effects - just data transformations.
5
+ */
6
+
7
+ import { AuthError, VizzlyError } from '../errors/vizzly-error.js';
8
+
9
+ // ============================================================================
10
+ // Header Building
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Build Authorization header from access token
15
+ * @param {string|null} accessToken - Access token
16
+ * @returns {Object} Headers object with Authorization if token exists
17
+ */
18
+ export function buildAuthHeader(accessToken) {
19
+ if (!accessToken) return {};
20
+ return {
21
+ Authorization: `Bearer ${accessToken}`
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Build User-Agent header for auth requests
27
+ * @param {string} version - CLI version
28
+ * @returns {string} User-Agent string
29
+ */
30
+ export function buildAuthUserAgent(version) {
31
+ return `vizzly-cli/${version} (auth)`;
32
+ }
33
+
34
+ /**
35
+ * Build complete headers for a request
36
+ * @param {Object} options - Header options
37
+ * @returns {Object} Complete headers object
38
+ */
39
+ export function buildRequestHeaders({
40
+ userAgent,
41
+ accessToken,
42
+ contentType,
43
+ extra = {}
44
+ }) {
45
+ return {
46
+ 'User-Agent': userAgent,
47
+ ...(accessToken ? buildAuthHeader(accessToken) : {}),
48
+ ...(contentType ? {
49
+ 'Content-Type': contentType
50
+ } : {}),
51
+ ...extra
52
+ };
53
+ }
54
+
55
+ // ============================================================================
56
+ // Error Parsing
57
+ // ============================================================================
58
+
59
+ /**
60
+ * Parse error from API response
61
+ * @param {number} status - HTTP status code
62
+ * @param {Object|string} body - Response body (parsed JSON or text)
63
+ * @param {string} endpoint - API endpoint for context
64
+ * @returns {Error} Appropriate error type
65
+ */
66
+ export function parseAuthError(status, body, _endpoint) {
67
+ let errorText = '';
68
+ if (typeof body === 'object' && body !== null) {
69
+ errorText = body.error || body.message || '';
70
+ } else if (typeof body === 'string') {
71
+ errorText = body;
72
+ }
73
+ if (status === 401) {
74
+ return new AuthError(errorText || 'Invalid credentials. Please check your email/username and password.');
75
+ }
76
+ if (status === 429) {
77
+ return new VizzlyError('Too many login attempts. Please try again later.', 'RATE_LIMIT_ERROR');
78
+ }
79
+ return new VizzlyError(`Authentication request failed: ${status}${errorText ? ` - ${errorText}` : ''}`, 'AUTH_REQUEST_ERROR');
80
+ }
81
+
82
+ /**
83
+ * Parse error for authenticated requests (different error messages)
84
+ * @param {number} status - HTTP status code
85
+ * @param {Object|string} body - Response body
86
+ * @param {string} endpoint - API endpoint
87
+ * @returns {Error} Appropriate error type
88
+ */
89
+ export function parseAuthenticatedError(status, body, endpoint) {
90
+ let errorText = '';
91
+ if (typeof body === 'object' && body !== null) {
92
+ errorText = body.error || body.message || '';
93
+ } else if (typeof body === 'string') {
94
+ errorText = body;
95
+ }
96
+ if (status === 401) {
97
+ return new AuthError('Authentication token is invalid or expired. Please run "vizzly login" again.');
98
+ }
99
+ return new VizzlyError(`API request failed: ${status}${errorText ? ` - ${errorText}` : ''} (${endpoint})`, 'API_REQUEST_ERROR');
100
+ }
101
+
102
+ // ============================================================================
103
+ // Payload Building
104
+ // ============================================================================
105
+
106
+ /**
107
+ * Build device poll request payload
108
+ * @param {string} deviceCode - Device code from initiate
109
+ * @returns {Object} Request payload
110
+ */
111
+ export function buildDevicePollPayload(deviceCode) {
112
+ return {
113
+ device_code: deviceCode
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Build refresh token request payload
119
+ * @param {string} refreshToken - Refresh token
120
+ * @returns {Object} Request payload
121
+ */
122
+ export function buildRefreshPayload(refreshToken) {
123
+ return {
124
+ refreshToken
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Build logout request payload
130
+ * @param {string} refreshToken - Refresh token to revoke
131
+ * @returns {Object} Request payload
132
+ */
133
+ export function buildLogoutPayload(refreshToken) {
134
+ return {
135
+ refreshToken
136
+ };
137
+ }
138
+
139
+ // ============================================================================
140
+ // Token Handling
141
+ // ============================================================================
142
+
143
+ /**
144
+ * Build token data for storage from API response
145
+ * @param {Object} response - API response with tokens
146
+ * @param {Object|null} existingUser - Existing user data to preserve
147
+ * @returns {Object} Token data for storage
148
+ */
149
+ export function buildTokenData(response, existingUser = null) {
150
+ return {
151
+ accessToken: response.accessToken,
152
+ refreshToken: response.refreshToken,
153
+ expiresAt: response.expiresAt,
154
+ user: response.user || existingUser
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Validate that tokens exist and have required fields
160
+ * @param {Object|null} auth - Auth tokens object
161
+ * @param {string} requiredField - Field that must exist ('accessToken' or 'refreshToken')
162
+ * @returns {{ valid: boolean, error: Error|null }}
163
+ */
164
+ export function validateTokens(auth, requiredField = 'accessToken') {
165
+ if (!auth || !auth[requiredField]) {
166
+ let message = requiredField === 'refreshToken' ? 'No refresh token found. Please run "vizzly login" first.' : 'No authentication token found. Please run "vizzly login" first.';
167
+ return {
168
+ valid: false,
169
+ error: new AuthError(message)
170
+ };
171
+ }
172
+ return {
173
+ valid: true,
174
+ error: null
175
+ };
176
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Auth Module - Public exports
3
+ *
4
+ * Provides functional authentication primitives:
5
+ * - core.js: Pure functions for headers, payloads, error parsing
6
+ * - client.js: HTTP client factory
7
+ * - operations.js: Auth operations with dependency injection
8
+ */
9
+
10
+ // Re-export token store utilities for convenience
11
+ import { clearAuthTokens, getAuthTokens, saveAuthTokens } from '../utils/global-config.js';
12
+ export { clearAuthTokens, getAuthTokens, saveAuthTokens };
13
+ // HTTP client factory
14
+ export { createAuthClient } from './client.js';
15
+ // Core pure functions
16
+ export { buildAuthHeader, buildAuthUserAgent, buildDevicePollPayload, buildLogoutPayload, buildRefreshPayload, buildRequestHeaders, buildTokenData, parseAuthError, parseAuthenticatedError, validateTokens } from './core.js';
17
+ // Auth operations (take dependencies as parameters)
18
+ export { completeDeviceFlow, initiateDeviceFlow, isAuthenticated, logout, pollDeviceAuthorization, refresh, whoami } from './operations.js';
19
+
20
+ /**
21
+ * Create a token store adapter from global-config functions
22
+ * Used by auth operations that need tokenStore parameter
23
+ */
24
+ export function createTokenStore() {
25
+ return {
26
+ getTokens: getAuthTokens,
27
+ saveTokens: saveAuthTokens,
28
+ clearTokens: clearAuthTokens
29
+ };
30
+ }