UNPKG

@letsparky/api-v2-client

Version:

TypeScript client for the LetsParky API V2

240 lines (239 loc) 9.34 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ApiClient = void 0; const fetch_1 = require("./adapters/fetch"); const types_1 = require("./types"); const logger_1 = require("./logger"); const errors_1 = require("./errors"); /** * Extract JWT expiration timestamp */ function extractJwtExpiration(token) { try { const parts = token.split('.'); if (parts.length !== 3) return null; const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); return payload.exp ? payload.exp * 1000 : null; } catch { return null; } } /** * Simplified API client for LetsParky API v2 */ class ApiClient { constructor(config = {}) { this.rateLimitInfo = null; const { apiKey, baseUrl, timeout = 30000, email, password, environment = types_1.Environment.PRODUCTION, logLevel = types_1.LogLevel.INFO, silentLogs = false } = config; this.apiKey = apiKey; this.email = email; this.password = password; this.environment = environment; this.timeout = timeout; this.logger = new logger_1.Logger({ level: logLevel, silent: silentLogs }); this.baseUrl = baseUrl || this.getDefaultBaseUrl(); // Auto-authenticate if email/password provided if (this.email && this.password) { this.authenticate().catch(err => { this.logger.error('Auto-authentication failed:', err); }); } } getDefaultBaseUrl() { const hosts = { [types_1.Environment.DEVELOPMENT]: 'http://localhost:3000', [types_1.Environment.STAGING]: 'https://test.api-v2.letsparky.com', [types_1.Environment.PRODUCTION]: 'https://api-v2.letsparky.com' }; return `${hosts[this.environment]}/api/v2`; } buildUrl(path, params) { // Ensure path starts with / and remove leading / from path to append properly const cleanPath = path.startsWith('/') ? path.substring(1) : path; // Ensure baseUrl ends with / for proper joining const baseUrlWithSlash = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`; const url = new URL(cleanPath, baseUrlWithSlash); if (params) { Object.entries(params).forEach(([key, value]) => { if (value != null) { url.searchParams.set(key, String(value)); } }); } return url.toString(); } createHeaders(customHeaders) { const headers = new Headers({ 'Accept': 'application/json', 'Content-Type': 'application/json', ...customHeaders }); // Add authentication if (this.authToken && this.authToken.token && this.authToken.expiresAt > Date.now()) { headers.set('Authorization', `Bearer ${this.authToken.token}`); } else if (this.apiKey) { headers.set('Authorization', `Bearer ${this.apiKey}`); } return headers; } async makeRequest(method, path, data, params) { const url = this.buildUrl(path, params); const headers = this.createHeaders(); const context = { method: method.toUpperCase(), url }; try { console.log("Making request to", url, "with token", this.authToken); // Check if token needs refresh if (this.authToken && this.authToken.expiresAt - Date.now() < 30000) { try { await this.refreshToken(); } catch (err) { this.logger.warn('Token refresh failed, removing auth:', err); this.setToken('', 0); // Clear token } } this.logger.debug('Making request:', { url, method, params }); const response = await (0, fetch_1.fetchWithTimeout)(url, { method, headers, body: data ? JSON.stringify(data) : undefined, timeout: this.timeout }); // Update rate limit info this.rateLimitInfo = (0, fetch_1.extractRateLimitInfo)(response.headers); const parsedResponse = await (0, fetch_1.parseResponse)(response); // Handle error status codes if (response.status >= 400) { if (response.status === 429) { const retryAfter = parseInt(response.headers.get('retry-after') || '60'); throw new errors_1.RateLimitError(`Rate limit exceeded. Retry after ${retryAfter} seconds.`, retryAfter, context); } if (response.status === 401 && this.email && this.password) { await this.authenticate(); return this.makeRequest(method, path, data, params); } const message = this.extractErrorMessage(parsedResponse.data); throw new errors_1.ApiError(message, 'API_ERROR', response.status, parsedResponse.data, context); } // Auto-capture tokens from auth responses if (path.includes('/auth/') && parsedResponse.data && typeof parsedResponse.data === 'object') { this.handleAuthResponse(parsedResponse.data); } return parsedResponse; } catch (error) { if (error instanceof errors_1.ApiError || error instanceof errors_1.RateLimitError) { throw error; } throw (0, errors_1.createStandardError)(error, 'Request failed', context); } } extractErrorMessage(data) { var _a; if (typeof data === 'string') return data; if (data === null || data === void 0 ? void 0 : data.message) return data.message; if ((_a = data === null || data === void 0 ? void 0 : data.error) === null || _a === void 0 ? void 0 : _a.message) return data.error.message; return 'API request failed'; } handleAuthResponse(data) { var _a, _b; const token = data.token || ((_a = data.data) === null || _a === void 0 ? void 0 : _a.token); const expiresIn = data.expiresIn || ((_b = data.data) === null || _b === void 0 ? void 0 : _b.expiresIn); if (token) { this.setToken(token, expiresIn); } } async refreshToken() { var _a; if (!this.email || !this.password) { throw new errors_1.AuthenticationError('Credentials required for token refresh', {}); } try { const response = await this.post('/auth/refresh', {}); if ((_a = response.data) === null || _a === void 0 ? void 0 : _a.token) { this.setToken(response.data.token, response.data.expiresIn); } } catch { // If refresh fails, do full auth await this.authenticate(); } } // Public methods setApiKey(apiKey) { this.apiKey = apiKey; } setToken(token, expiresIn) { if (!token) { this.authToken = undefined; return; } console.log("Setting token", token); const expiresAt = expiresIn ? Date.now() + (expiresIn * 1000) : extractJwtExpiration(token) || Date.now() + 3600000; this.authToken = { token, expiresAt }; } getToken() { var _a; return ((_a = this.authToken) === null || _a === void 0 ? void 0 : _a.token) || null; } isAuthenticated() { return !!(this.apiKey || (this.authToken && this.authToken.token && this.authToken.expiresAt > Date.now())); } setEnvironment(environment) { this.environment = environment; this.baseUrl = this.baseUrl || this.getDefaultBaseUrl(); } setLogLevel(level) { this.logger.setLevel(level); } async authenticate() { var _a; if (!this.email || !this.password) { throw new errors_1.AuthenticationError('Email and password required', {}); } const response = await this.post('/auth/login', { email: this.email, password: this.password }); if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.token)) { throw new errors_1.AuthenticationError('No token received', {}); } this.setToken(response.data.token, response.data.expiresIn); } getRateLimitInfo() { return this.rateLimitInfo; } // HTTP methods async get(path, params) { return this.makeRequest('GET', path, undefined, params); } async getPaginated(path, paginationParams, queryParams) { const params = { ...queryParams, ...paginationParams }; const response = await this.makeRequest('GET', path, undefined, params); return response.data; } async post(path, data) { return this.makeRequest('POST', path, data); } async put(path, data) { return this.makeRequest('PUT', path, data); } async delete(path, data) { return this.makeRequest('DELETE', path, data); } } exports.ApiClient = ApiClient;