@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.
- package/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +178 -3
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +121 -36
- package/dist/commands/finalize.js +49 -18
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +49 -55
- package/dist/commands/logout.js +17 -9
- package/dist/commands/project.js +100 -71
- package/dist/commands/run.js +189 -95
- package/dist/commands/status.js +101 -66
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +104 -98
- package/dist/commands/upload.js +78 -34
- package/dist/commands/whoami.js +44 -42
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +191 -53
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +186 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +209 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/test-runner.js +90 -250
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1145 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/types/client.d.ts +25 -2
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +13 -13
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/static-report-generator.js +0 -207
- 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
|
+
}
|