tdpw
Version:
CLI tool for uploading Playwright test reports to TestDino platform with Azure storage support
208 lines • 8.51 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SasTokenService = void 0;
const types_1 = require("../types");
// Endpoint for SAS token
const SAS_ENDPOINT = '/api/storage/token';
/**
* Service to request and manage Azure SAS tokens from TestDino API
*/
class SasTokenService {
config;
baseUrl;
apiKey;
constructor(config) {
this.config = config;
this.baseUrl = config.apiUrl;
this.apiKey = config.token;
}
/**
* Headers to include on every SAS token request
*/
getHeaders() {
return {
'Content-Type': 'application/json',
'User-Agent': 'tdpw/1.0.0',
'X-API-Key': this.apiKey, // Fixed: was 'X-API-key', now matches curl
};
}
/**
* Request a SAS token from TestDino API using POST method
*/
async requestSasToken() {
const url = `${this.baseUrl}${SAS_ENDPOINT}`;
let response;
try {
// Fixed: Use POST method with proper headers as shown in curl
response = await fetch(url, {
method: 'POST', // Changed from GET to POST
headers: this.getHeaders(),
// Empty body for POST request
body: JSON.stringify({}),
});
}
catch (error) {
throw new types_1.NetworkError(`Network error requesting SAS token: ${error.message}`);
}
// Handle HTTP error responses
if (!response.ok) {
await this.handleSasError(response);
}
// Parse JSON response
let data;
try {
data = await response.json();
}
catch (error) {
throw new types_1.NetworkError(`Failed to parse SAS token response: ${error.message}`);
}
// Validate and extract SAS token data
return this.validateSasResponse(data);
}
/**
* Handle HTTP error responses with detailed error messages
*/
async handleSasError(response) {
let errorBody;
try {
errorBody = await response.text();
}
catch {
errorBody = 'Unable to read error response';
}
switch (response.status) {
case 401:
throw new types_1.AuthenticationError('Invalid API key for SAS token request');
case 403:
throw new types_1.AuthenticationError('API key does not have permission to request SAS tokens');
case 400:
throw new types_1.NetworkError(`Bad SAS token request: ${errorBody}`);
case 404:
throw new types_1.NetworkError(`SAS token endpoint not found (${response.status}): ${errorBody}`);
case 429:
throw new types_1.NetworkError('Rate limit exceeded for SAS token requests');
case 500:
case 502:
case 503:
case 504:
throw new types_1.NetworkError(`Server error requesting SAS token (${response.status}) - please try again later`);
default:
throw new types_1.NetworkError(`SAS token request failed: HTTP ${response.status} - ${errorBody}`);
}
}
/**
* Validate and parse the SAS token response (updated for actual API format)
*/
validateSasResponse(data) {
if (!data || typeof data !== 'object') {
throw new types_1.NetworkError('Invalid SAS token response format - expected JSON object');
}
const response = data;
// Check for success wrapper format
if (response.success !== true || response.data == null || typeof response.data !== 'object') {
const message = typeof response.message === 'string' ? response.message : 'Unknown error';
throw new types_1.NetworkError(`SAS token request failed: ${message}`);
}
const sasData = response.data;
// Validate required fields based on actual API response
const requiredFields = ['tokenId', 'sasToken', 'containerUrl', 'blobPath', 'uniqueId', 'expiresAt', 'uploadInstructions'];
const missingFields = requiredFields.filter(field => !(field in sasData));
if (missingFields.length > 0) {
throw new types_1.NetworkError(`SAS token response missing required fields: ${missingFields.join(', ')}`);
}
// Validate field types
if (typeof sasData.tokenId !== 'string') {
throw new types_1.NetworkError('SAS token response: tokenId must be a string');
}
if (typeof sasData.sasToken !== 'string') {
throw new types_1.NetworkError('SAS token response: sasToken must be a string');
}
if (typeof sasData.containerUrl !== 'string') {
throw new types_1.NetworkError('SAS token response: containerUrl must be a string');
}
if (typeof sasData.expiresAt !== 'string') {
throw new types_1.NetworkError('SAS token response: expiresAt must be a string');
}
// Validate container URL format
try {
new URL(sasData.containerUrl);
}
catch {
throw new types_1.NetworkError(`Invalid containerUrl format: ${sasData.containerUrl}`);
}
// Validate upload instructions
if (!sasData.uploadInstructions || typeof sasData.uploadInstructions !== 'object') {
throw new types_1.NetworkError('SAS token response: uploadInstructions must be an object');
}
const instructions = sasData.uploadInstructions;
if (!instructions.baseUrl || !instructions.pathPrefix) {
throw new types_1.NetworkError('SAS token response: uploadInstructions missing baseUrl or pathPrefix');
}
// Prepare exampleUpload with proper type checks
const exampleUploadObj = {
method: 'PUT',
url: '',
headers: {},
};
if (instructions.exampleUpload && typeof instructions.exampleUpload === 'object') {
const eu = instructions.exampleUpload;
if (typeof eu.method === 'string')
exampleUploadObj.method = eu.method;
if (typeof eu.url === 'string')
exampleUploadObj.url = eu.url;
if (eu.headers && typeof eu.headers === 'object') {
exampleUploadObj.headers = eu.headers;
}
}
const result = {
tokenId: sasData.tokenId,
sasToken: sasData.sasToken,
containerUrl: sasData.containerUrl,
blobPath: sasData.blobPath,
uniqueId: sasData.uniqueId,
expiresAt: sasData.expiresAt,
maxSize: typeof sasData.maxSize === 'number' ? sasData.maxSize : 2147483648, // 2GB default
permissions: Array.isArray(sasData.permissions) ? sasData.permissions : [],
uploadInstructions: {
baseUrl: instructions.baseUrl,
pathPrefix: instructions.pathPrefix,
allowedFileTypes: Array.isArray(instructions.allowedFileTypes) ? instructions.allowedFileTypes : [],
maxFileSize: typeof instructions.maxFileSize === 'number' ? instructions.maxFileSize : (typeof sasData.maxSize === 'number' ? sasData.maxSize : 2147483648),
exampleUpload: exampleUploadObj
}
};
return result;
}
/**
* Check if SAS token is expired (updated for actual API format)
*/
isTokenExpired(sasResponse) {
try {
const expirationDate = new Date(sasResponse.expiresAt);
const now = new Date();
// Add small buffer (5 minutes) to account for clock skew
const bufferMs = 5 * 60 * 1000;
return (expirationDate.getTime() - bufferMs) <= now.getTime();
}
catch {
// If we can't parse the date, assume it's expired for safety
return true;
}
}
/**
* Get time until SAS token expires (in minutes)
*/
getTimeUntilExpiry(sasResponse) {
try {
const expirationDate = new Date(sasResponse.expiresAt);
const now = new Date();
const diffMs = expirationDate.getTime() - now.getTime();
return Math.floor(diffMs / (60 * 1000)); // Convert to minutes
}
catch {
return 0;
}
}
}
exports.SasTokenService = SasTokenService;
//# sourceMappingURL=sas.js.map