const https = require('https'); function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function maskKey(key) { if (!key) return '[NO_KEY]'; return key.length > 12 ? `${key.slice(0, 8)}...${key.slice(-4)}` : '***'; } class PlaneClient { constructor(config) { this.apiKey = config.apiKey; this.host = config.host; this.workspace = config.workspace; this.verbose = config.verbose || false; this.lastRequestTime = 0; this.minDelay = 500; } async request(apiPath) { // WHY: keep a small delay between requests to respect Plane's rate limits const wait = this.minDelay - (Date.now() - this.lastRequestTime); if (wait > 0) await delay(wait); this.lastRequestTime = Date.now(); if (this.verbose) console.log(`🔗 GET ${apiPath}`); return new Promise((resolve, reject) => { const url = new URL(`${this.host}${apiPath}`); const req = https.request( { hostname: url.hostname, port: url.port, path: url.pathname + url.search, method: 'GET', headers: { 'x-api-key': this.apiKey, 'Accept': 'application/json' } }, res => { let body = ''; res.on('data', chunk => (body += chunk)); res.on('end', () => { if (res.statusCode >= 400) { return reject(new Error(`Plane API error ${res.statusCode}: ${body.slice(0, 200)}`)); } try { resolve(JSON.parse(body)); } catch (error) { reject(new Error(`Invalid JSON response (key: ${maskKey(this.apiKey)}): ${body.slice(0, 120)}`)); } }); } ); req.on('error', reject); req.end(); }); } async getProjects() { const data = await this.request(`/api/v1/workspaces/${this.workspace}/projects/`); return data.results || []; } async getIssues(projectId) { const issues = []; let cursor = null; do { const base = `/api/v1/workspaces/${this.workspace}/projects/${projectId}/issues/`; const query = `per_page=100&expand=state${cursor ? `&cursor=${cursor}` : ''}`; const data = await this.request(`${base}?${query}`); issues.push(...(data.results || [])); cursor = data.next_page_results ? data.next_cursor : null; } while (cursor); return issues; } } module.exports = { PlaneClient, maskKey };