@arcadialdev/arcality 2.6.6 → 3.0.0

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.
@@ -93,6 +93,16 @@ function getEffectiveApiKey() {
93
93
  return key;
94
94
  }
95
95
 
96
+ /**
97
+ * Gets the effective Project ID.
98
+ * Priority: arcality.config > ARCALITY_PROJECT_ID env var
99
+ */
100
+ function loadProjectId() {
101
+ const localConfig = loadArcalityConfig();
102
+ if (localConfig?.projectId) return localConfig.projectId;
103
+ return process.env.ARCALITY_PROJECT_ID || null;
104
+ }
105
+
96
106
  // ═══════════════════════════════════════════════════════
97
107
  // PUBLIC API
98
108
  // ═══════════════════════════════════════════════════════
@@ -244,6 +254,54 @@ export async function startMission(prompt, targetUrl) {
244
254
  };
245
255
  }
246
256
 
257
+ /**
258
+ * Fetches missions from the backend API.
259
+ * @param {string} projectId
260
+ * @param {string} tags - Comma separated tags (optional)
261
+ * @returns {Promise<Object>} Contains success flag and missions array
262
+ */
263
+ export async function fetchMissions(projectId, tags) {
264
+ const key = getEffectiveApiKey();
265
+ if (!key) return { success: false, error: 'no_api_key', missions: [] };
266
+
267
+ const apiBase = getEffectiveApiBase();
268
+ if (!apiBase) {
269
+ // MOCK MODE: Return empty array for now since there's no backend
270
+ return { success: true, mode: 'mock', missions: [] };
271
+ }
272
+
273
+ try {
274
+ const url = new URL(`${apiBase}/api/v1/projects/${projectId}/missions`);
275
+ if (tags) {
276
+ url.searchParams.append('tags', tags);
277
+ }
278
+
279
+ const controller = new AbortController();
280
+ const timeout = setTimeout(() => controller.abort(), 10000);
281
+ const res = await fetch(url.toString(), {
282
+ method: 'GET',
283
+ headers: {
284
+ 'x-api-key': key
285
+ },
286
+ signal: controller.signal
287
+ });
288
+ clearTimeout(timeout);
289
+
290
+ if (!res.ok) {
291
+ const errText = await res.text();
292
+ console.warn(`[DEBUG] fetchMissions failed: ${res.status} - ${errText}`);
293
+ return { success: false, error: 'server_error', missions: [] };
294
+ }
295
+
296
+ const data = await res.json();
297
+ return { success: true, missions: data.missions || [] };
298
+ } catch (e) {
299
+ const reason = e?.name === 'AbortError' ? 'timeout (10s)' : (e?.message || 'unknown');
300
+ console.warn(`⚠️ fetchMissions failed (${apiBase}): ${reason}.`);
301
+ return { success: false, error: reason, missions: [] };
302
+ }
303
+ }
304
+
247
305
  /**
248
306
  * Marks a mission as completed.
249
307
  * @param {string} missionId
@@ -281,6 +339,75 @@ function simpleHash(str) {
281
339
  return 'ph_' + Math.abs(hash).toString(36);
282
340
  }
283
341
 
342
+ /**
343
+ * Crea una Task en Azure DevOps cuando se detecta un bug en el portal.
344
+ * Si la organización no tiene la integración configurada (HTTP 400) se registra
345
+ * sólo en modo DEBUG y NO interrumpe el flujo del test runner.
346
+ *
347
+ * @param {string} title - Título de la tarea (generalmente la descripción corta del bug)
348
+ * @param {string} description - Descripción detallada del bug detectado
349
+ * @returns {Promise<{success: boolean, taskUrl?: string, error?: string}>}
350
+ */
351
+ export async function createAdoTask(title, description) {
352
+ const apiBase = getEffectiveApiBase();
353
+ if (!apiBase) {
354
+ if (process.env.DEBUG) console.log(`[ADO] No hay ARCALITY_API_URL configurado. Omitiendo creación de Task en Azure DevOps.`);
355
+ return { success: false, error: 'no_api_url' };
356
+ }
357
+
358
+ const projectId = loadProjectId();
359
+ if (!projectId) {
360
+ if (process.env.DEBUG) console.log(`[ADO] No hay Project ID configurado. Omitiendo creación de Task en Azure DevOps.`);
361
+ return { success: false, error: 'no_project_id' };
362
+ }
363
+
364
+ const key = getEffectiveApiKey();
365
+ if (!key) {
366
+ if (process.env.DEBUG) console.log(`[ADO] No hay API key configurada. Omitiendo creación de Task en Azure DevOps.`);
367
+ return { success: false, error: 'no_api_key' };
368
+ }
369
+
370
+ try {
371
+ const controller = new AbortController();
372
+ const timeout = setTimeout(() => controller.abort(), 15000);
373
+
374
+ const res = await fetch(`${apiBase}/api/v1/integrations/azuredevops/task`, {
375
+ method: 'POST',
376
+ headers: {
377
+ 'Content-Type': 'application/json',
378
+ 'x-api-key': key
379
+ },
380
+ body: JSON.stringify({ project_id: projectId, title, description }),
381
+ signal: controller.signal
382
+ });
383
+ clearTimeout(timeout);
384
+
385
+ if (res.status === 400) {
386
+ const data = await res.json().catch(() => ({}));
387
+ // La organización no tiene la integración de ADO configurada — no es un error crítico
388
+ if (data?.error === 'create_task_failed') {
389
+ if (process.env.DEBUG) console.log(`[ADO] Integración no configurada para esta organización: ${data.message}`);
390
+ } else {
391
+ console.warn(`⚠️ [ADO] Error al crear Task en Azure DevOps: ${data?.message || 'Bad Request'}`);
392
+ }
393
+ return { success: false, error: data?.message || 'create_task_failed' };
394
+ }
395
+
396
+ if (!res.ok) {
397
+ console.warn(`⚠️ [ADO] Error HTTP ${res.status} al crear Task en Azure DevOps.`);
398
+ return { success: false, error: `http_${res.status}` };
399
+ }
400
+
401
+ const data = await res.json();
402
+ console.log(`✅ [ADO] Task creada en Azure DevOps: ${data.task_url || '(sin URL)'}`);
403
+ return { success: true, taskUrl: data.task_url };
404
+ } catch (e) {
405
+ const reason = e?.name === 'AbortError' ? 'timeout (15s)' : (e?.message || 'unknown');
406
+ console.warn(`⚠️ [ADO] No se pudo crear la Task en Azure DevOps: ${reason}`);
407
+ return { success: false, error: reason };
408
+ }
409
+ }
410
+
284
411
  /**
285
412
  * Obtiene el Token SAS y el path base para subir evidencias.
286
413
  * @param {string} missionId
@@ -309,7 +436,7 @@ export async function getEvidenceSasToken(missionId, projectId) {
309
436
  console.log(`[DEBUG] SAS Token API Failed: ${res.status} - ${txt}`);
310
437
  return null;
311
438
  }
312
-
439
+
313
440
  const data = await res.json();
314
441
  return data;
315
442
  } catch (e) {
@@ -18,8 +18,7 @@ export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
18
18
  * @returns {string}
19
19
  */
20
20
  export function getApiUrl() {
21
- // return 'https://arcalityqadev.arcadial.lat';
22
- return 'https://arcalityqadev.arcadial.lat';
21
+ return 'https://arcalityqadev.arcadial.lat';
23
22
  }
24
23
 
25
24
  /**
@@ -66,6 +65,11 @@ export function loadConfig() {
66
65
  return config;
67
66
  }
68
67
 
68
+ export function loadProjectId() {
69
+ const localConfig = loadLocalArcalityConfig();
70
+ return localConfig?.projectId || process.env.ARCALITY_PROJECT_ID;
71
+ }
72
+
69
73
 
70
74
  /**
71
75
  * Reads the user's global config (~/.arcality/config.json)
@@ -127,7 +127,7 @@ export function injectConfigToEnv(config) {
127
127
 
128
128
  if (config.apiKey) process.env.ARCALITY_API_KEY = config.apiKey;
129
129
  if (config.projectId) process.env.ARCALITY_PROJECT_ID = config.projectId;
130
- if (config.project?.baseUrl) {
130
+ if (config.project?.baseUrl && !process.env.BASE_URL) {
131
131
  process.env.BASE_URL = config.project.baseUrl;
132
132
  }
133
133
  if (config.auth?.username) process.env.LOGIN_USER = config.auth.username;
@@ -23,6 +23,10 @@ export class SecurityScanner {
23
23
  * @returns A promise that resolves to an array of found vulnerabilities.
24
24
  */
25
25
  async runAllScans(): Promise<SecurityFinding[]> {
26
+ if (this.page.isClosed()) {
27
+ if (process.env.DEBUG) console.log('[QA-SEC] Security scan skipped because page is closed.');
28
+ return [];
29
+ }
26
30
  if (process.env.DEBUG) console.log('[QA-SEC] Starting comprehensive security scan...');
27
31
 
28
32
  // Scan 1: Audit HTTP Security Headers
@@ -46,6 +50,7 @@ export class SecurityScanner {
46
50
  * A basic test for open redirect vulnerabilities on the current URL.
47
51
  */
48
52
  async testOpenRedirect(): Promise<void> {
53
+ if (this.page.isClosed()) return;
49
54
  const url = new URL(this.page.url());
50
55
  const redirectParams = ['redirect', 'next', 'url', 'returnTo', 'dest'];
51
56