@bbearai/core 0.1.6 → 0.2.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.
- package/dist/index.d.mts +201 -1
- package/dist/index.d.ts +201 -1
- package/dist/index.js +160 -11
- package/dist/index.mjs +160 -11
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -263,6 +263,185 @@ interface TesterMessage {
|
|
|
263
263
|
name: string;
|
|
264
264
|
}>;
|
|
265
265
|
}
|
|
266
|
+
/** Priority factors breakdown for a route */
|
|
267
|
+
interface PriorityFactors {
|
|
268
|
+
bugFrequency: {
|
|
269
|
+
score: number;
|
|
270
|
+
openBugs: number;
|
|
271
|
+
bugs7d: number;
|
|
272
|
+
};
|
|
273
|
+
criticalSeverity: {
|
|
274
|
+
score: number;
|
|
275
|
+
critical: number;
|
|
276
|
+
high: number;
|
|
277
|
+
};
|
|
278
|
+
staleness: {
|
|
279
|
+
score: number;
|
|
280
|
+
daysSinceTest: number | null;
|
|
281
|
+
};
|
|
282
|
+
coverageGap: {
|
|
283
|
+
score: number;
|
|
284
|
+
testCount: number;
|
|
285
|
+
};
|
|
286
|
+
regressionRisk: {
|
|
287
|
+
score: number;
|
|
288
|
+
regressionCount: number;
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/** Route priority with stats and recommendations */
|
|
292
|
+
interface RoutePriority {
|
|
293
|
+
rank: number;
|
|
294
|
+
route: string;
|
|
295
|
+
priorityScore: number;
|
|
296
|
+
urgency: 'critical' | 'high' | 'medium' | 'low';
|
|
297
|
+
stats: {
|
|
298
|
+
openBugs: number;
|
|
299
|
+
criticalBugs: number;
|
|
300
|
+
highBugs: number;
|
|
301
|
+
testCases: number;
|
|
302
|
+
daysSinceTest: number | null;
|
|
303
|
+
regressions: number;
|
|
304
|
+
recentBugs: number;
|
|
305
|
+
};
|
|
306
|
+
factors?: PriorityFactors;
|
|
307
|
+
recommendation?: string;
|
|
308
|
+
}
|
|
309
|
+
/** Coverage gap identification */
|
|
310
|
+
interface CoverageGap {
|
|
311
|
+
route: string;
|
|
312
|
+
severity: 'critical' | 'high' | 'medium';
|
|
313
|
+
type: 'untested' | 'missing_tracks' | 'stale';
|
|
314
|
+
details: {
|
|
315
|
+
missingTracks?: string[];
|
|
316
|
+
daysSinceTest?: number;
|
|
317
|
+
openBugs?: number;
|
|
318
|
+
criticalBugs?: number;
|
|
319
|
+
};
|
|
320
|
+
recommendation: string;
|
|
321
|
+
}
|
|
322
|
+
/** Regression event tracking */
|
|
323
|
+
interface RegressionEvent {
|
|
324
|
+
id: string;
|
|
325
|
+
route: string;
|
|
326
|
+
severity: 'critical' | 'high' | 'medium';
|
|
327
|
+
originalBug: {
|
|
328
|
+
id: string;
|
|
329
|
+
description: string;
|
|
330
|
+
resolvedAt: string;
|
|
331
|
+
};
|
|
332
|
+
newBugs: Array<{
|
|
333
|
+
id: string;
|
|
334
|
+
description: string;
|
|
335
|
+
severity: string;
|
|
336
|
+
createdAt: string;
|
|
337
|
+
}>;
|
|
338
|
+
daysSinceResolution: number;
|
|
339
|
+
regressionCount: number;
|
|
340
|
+
detectedAt: string;
|
|
341
|
+
}
|
|
342
|
+
/** Deploy checklist item */
|
|
343
|
+
interface ChecklistItem {
|
|
344
|
+
testCaseId?: string;
|
|
345
|
+
testKey?: string;
|
|
346
|
+
title: string;
|
|
347
|
+
route: string;
|
|
348
|
+
reason: string;
|
|
349
|
+
priority: 'P0' | 'P1' | 'P2' | 'P3';
|
|
350
|
+
lastTested?: string;
|
|
351
|
+
hasCriticalBugs?: boolean;
|
|
352
|
+
}
|
|
353
|
+
/** Deploy checklist grouped by urgency */
|
|
354
|
+
interface DeployChecklist {
|
|
355
|
+
critical: ChecklistItem[];
|
|
356
|
+
recommended: ChecklistItem[];
|
|
357
|
+
optional: ChecklistItem[];
|
|
358
|
+
gaps: ChecklistItem[];
|
|
359
|
+
}
|
|
360
|
+
/** QA Health metrics with trends */
|
|
361
|
+
interface QAHealthMetrics {
|
|
362
|
+
velocity: {
|
|
363
|
+
testsPerWeek: number;
|
|
364
|
+
testsCompleted: number;
|
|
365
|
+
trend: 'up' | 'down' | 'stable';
|
|
366
|
+
changePercent?: number;
|
|
367
|
+
};
|
|
368
|
+
bugDiscovery: {
|
|
369
|
+
bugsFound: number;
|
|
370
|
+
bugsPerTest: number;
|
|
371
|
+
criticalBugs: number;
|
|
372
|
+
trend: 'up' | 'down' | 'stable';
|
|
373
|
+
changePercent?: number;
|
|
374
|
+
};
|
|
375
|
+
resolution: {
|
|
376
|
+
bugsResolved: number;
|
|
377
|
+
avgResolutionDays: number;
|
|
378
|
+
trend: 'up' | 'down' | 'stable';
|
|
379
|
+
changePercent?: number;
|
|
380
|
+
};
|
|
381
|
+
coverage: {
|
|
382
|
+
routeCoverage: number;
|
|
383
|
+
routesWithTests: number;
|
|
384
|
+
totalRoutes: number;
|
|
385
|
+
};
|
|
386
|
+
testerHealth: {
|
|
387
|
+
activeTesters: number;
|
|
388
|
+
totalTesters: number;
|
|
389
|
+
utilizationPercent: number;
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/** QA Health score with grade */
|
|
393
|
+
interface QAHealthScore {
|
|
394
|
+
score: number;
|
|
395
|
+
grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
396
|
+
breakdown: {
|
|
397
|
+
coverage: number;
|
|
398
|
+
velocity: number;
|
|
399
|
+
resolution: number;
|
|
400
|
+
stability: number;
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/** Aggregated route test statistics */
|
|
404
|
+
interface RouteTestStats {
|
|
405
|
+
id: string;
|
|
406
|
+
projectId: string;
|
|
407
|
+
route: string;
|
|
408
|
+
totalBugs: number;
|
|
409
|
+
openBugs: number;
|
|
410
|
+
criticalBugs: number;
|
|
411
|
+
highBugs: number;
|
|
412
|
+
resolvedBugs: number;
|
|
413
|
+
testCaseCount: number;
|
|
414
|
+
testsByTrack: Record<string, number>;
|
|
415
|
+
lastTestedAt: string | null;
|
|
416
|
+
totalTestExecutions: number;
|
|
417
|
+
passCount: number;
|
|
418
|
+
failCount: number;
|
|
419
|
+
passRate: number | null;
|
|
420
|
+
regressionCount: number;
|
|
421
|
+
lastRegressionAt: string | null;
|
|
422
|
+
bugsLast7Days: number;
|
|
423
|
+
bugsLast30Days: number;
|
|
424
|
+
priorityScore: number;
|
|
425
|
+
calculatedAt: string;
|
|
426
|
+
}
|
|
427
|
+
/** Coverage matrix cell */
|
|
428
|
+
interface CoverageMatrixCell {
|
|
429
|
+
testCount: number;
|
|
430
|
+
passCount: number;
|
|
431
|
+
failCount: number;
|
|
432
|
+
passRate: number | null;
|
|
433
|
+
lastTestedAt: string | null;
|
|
434
|
+
staleDays: number | null;
|
|
435
|
+
}
|
|
436
|
+
/** Coverage matrix row (route × tracks) */
|
|
437
|
+
interface CoverageMatrixRow {
|
|
438
|
+
route: string;
|
|
439
|
+
totalTests: number;
|
|
440
|
+
openBugs: number;
|
|
441
|
+
criticalBugs: number;
|
|
442
|
+
lastTestedAt: string | null;
|
|
443
|
+
tracks: Record<string, CoverageMatrixCell>;
|
|
444
|
+
}
|
|
266
445
|
|
|
267
446
|
/**
|
|
268
447
|
* BugBear Client
|
|
@@ -304,8 +483,29 @@ declare class BugBearClient {
|
|
|
304
483
|
/**
|
|
305
484
|
* Get current tester info
|
|
306
485
|
* Looks up tester by email from the host app's authenticated user
|
|
486
|
+
* Checks both primary email AND additional_emails array
|
|
487
|
+
* Uses parameterized RPC function to prevent SQL injection
|
|
307
488
|
*/
|
|
308
489
|
getTesterInfo(): Promise<TesterInfo | null>;
|
|
490
|
+
/**
|
|
491
|
+
* Basic email format validation (defense in depth)
|
|
492
|
+
*/
|
|
493
|
+
private isValidEmail;
|
|
494
|
+
/**
|
|
495
|
+
* Validate report input before submission
|
|
496
|
+
* Returns error message if invalid, null if valid
|
|
497
|
+
*/
|
|
498
|
+
private validateReport;
|
|
499
|
+
/**
|
|
500
|
+
* Validate profile update input
|
|
501
|
+
* Returns error message if invalid, null if valid
|
|
502
|
+
*/
|
|
503
|
+
private validateProfileUpdate;
|
|
504
|
+
/**
|
|
505
|
+
* Check rate limit for an action
|
|
506
|
+
* Returns { allowed: boolean, error?: string, remaining?: number }
|
|
507
|
+
*/
|
|
508
|
+
private checkRateLimit;
|
|
309
509
|
/**
|
|
310
510
|
* Update tester profile
|
|
311
511
|
* Allows testers to update their name, additional emails, avatar, and platforms
|
|
@@ -467,4 +667,4 @@ declare function captureError(error: Error, errorInfo?: {
|
|
|
467
667
|
componentStack?: string;
|
|
468
668
|
};
|
|
469
669
|
|
|
470
|
-
export { type AppContext, BugBearClient, type BugBearConfig, type BugBearReport, type BugBearTheme, type ChecklistResult, type ConsoleLogEntry, type DeviceInfo, type EnhancedBugContext, type HostUserInfo, type MessageSenderType, type NetworkRequest, type QATrack, type ReportStatus, type ReportType, type RubricMode, type RubricResult, type Severity, type TestAssignment, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, captureError, contextCapture, createBugBear };
|
|
670
|
+
export { type AppContext, BugBearClient, type BugBearConfig, type BugBearReport, type BugBearTheme, type ChecklistItem, type ChecklistResult, type ConsoleLogEntry, type CoverageGap, type CoverageMatrixCell, type CoverageMatrixRow, type DeployChecklist, type DeviceInfo, type EnhancedBugContext, type HostUserInfo, type MessageSenderType, type NetworkRequest, type PriorityFactors, type QAHealthMetrics, type QAHealthScore, type QATrack, type RegressionEvent, type ReportStatus, type ReportType, type RoutePriority, type RouteTestStats, type RubricMode, type RubricResult, type Severity, type TestAssignment, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, captureError, contextCapture, createBugBear };
|
package/dist/index.d.ts
CHANGED
|
@@ -263,6 +263,185 @@ interface TesterMessage {
|
|
|
263
263
|
name: string;
|
|
264
264
|
}>;
|
|
265
265
|
}
|
|
266
|
+
/** Priority factors breakdown for a route */
|
|
267
|
+
interface PriorityFactors {
|
|
268
|
+
bugFrequency: {
|
|
269
|
+
score: number;
|
|
270
|
+
openBugs: number;
|
|
271
|
+
bugs7d: number;
|
|
272
|
+
};
|
|
273
|
+
criticalSeverity: {
|
|
274
|
+
score: number;
|
|
275
|
+
critical: number;
|
|
276
|
+
high: number;
|
|
277
|
+
};
|
|
278
|
+
staleness: {
|
|
279
|
+
score: number;
|
|
280
|
+
daysSinceTest: number | null;
|
|
281
|
+
};
|
|
282
|
+
coverageGap: {
|
|
283
|
+
score: number;
|
|
284
|
+
testCount: number;
|
|
285
|
+
};
|
|
286
|
+
regressionRisk: {
|
|
287
|
+
score: number;
|
|
288
|
+
regressionCount: number;
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/** Route priority with stats and recommendations */
|
|
292
|
+
interface RoutePriority {
|
|
293
|
+
rank: number;
|
|
294
|
+
route: string;
|
|
295
|
+
priorityScore: number;
|
|
296
|
+
urgency: 'critical' | 'high' | 'medium' | 'low';
|
|
297
|
+
stats: {
|
|
298
|
+
openBugs: number;
|
|
299
|
+
criticalBugs: number;
|
|
300
|
+
highBugs: number;
|
|
301
|
+
testCases: number;
|
|
302
|
+
daysSinceTest: number | null;
|
|
303
|
+
regressions: number;
|
|
304
|
+
recentBugs: number;
|
|
305
|
+
};
|
|
306
|
+
factors?: PriorityFactors;
|
|
307
|
+
recommendation?: string;
|
|
308
|
+
}
|
|
309
|
+
/** Coverage gap identification */
|
|
310
|
+
interface CoverageGap {
|
|
311
|
+
route: string;
|
|
312
|
+
severity: 'critical' | 'high' | 'medium';
|
|
313
|
+
type: 'untested' | 'missing_tracks' | 'stale';
|
|
314
|
+
details: {
|
|
315
|
+
missingTracks?: string[];
|
|
316
|
+
daysSinceTest?: number;
|
|
317
|
+
openBugs?: number;
|
|
318
|
+
criticalBugs?: number;
|
|
319
|
+
};
|
|
320
|
+
recommendation: string;
|
|
321
|
+
}
|
|
322
|
+
/** Regression event tracking */
|
|
323
|
+
interface RegressionEvent {
|
|
324
|
+
id: string;
|
|
325
|
+
route: string;
|
|
326
|
+
severity: 'critical' | 'high' | 'medium';
|
|
327
|
+
originalBug: {
|
|
328
|
+
id: string;
|
|
329
|
+
description: string;
|
|
330
|
+
resolvedAt: string;
|
|
331
|
+
};
|
|
332
|
+
newBugs: Array<{
|
|
333
|
+
id: string;
|
|
334
|
+
description: string;
|
|
335
|
+
severity: string;
|
|
336
|
+
createdAt: string;
|
|
337
|
+
}>;
|
|
338
|
+
daysSinceResolution: number;
|
|
339
|
+
regressionCount: number;
|
|
340
|
+
detectedAt: string;
|
|
341
|
+
}
|
|
342
|
+
/** Deploy checklist item */
|
|
343
|
+
interface ChecklistItem {
|
|
344
|
+
testCaseId?: string;
|
|
345
|
+
testKey?: string;
|
|
346
|
+
title: string;
|
|
347
|
+
route: string;
|
|
348
|
+
reason: string;
|
|
349
|
+
priority: 'P0' | 'P1' | 'P2' | 'P3';
|
|
350
|
+
lastTested?: string;
|
|
351
|
+
hasCriticalBugs?: boolean;
|
|
352
|
+
}
|
|
353
|
+
/** Deploy checklist grouped by urgency */
|
|
354
|
+
interface DeployChecklist {
|
|
355
|
+
critical: ChecklistItem[];
|
|
356
|
+
recommended: ChecklistItem[];
|
|
357
|
+
optional: ChecklistItem[];
|
|
358
|
+
gaps: ChecklistItem[];
|
|
359
|
+
}
|
|
360
|
+
/** QA Health metrics with trends */
|
|
361
|
+
interface QAHealthMetrics {
|
|
362
|
+
velocity: {
|
|
363
|
+
testsPerWeek: number;
|
|
364
|
+
testsCompleted: number;
|
|
365
|
+
trend: 'up' | 'down' | 'stable';
|
|
366
|
+
changePercent?: number;
|
|
367
|
+
};
|
|
368
|
+
bugDiscovery: {
|
|
369
|
+
bugsFound: number;
|
|
370
|
+
bugsPerTest: number;
|
|
371
|
+
criticalBugs: number;
|
|
372
|
+
trend: 'up' | 'down' | 'stable';
|
|
373
|
+
changePercent?: number;
|
|
374
|
+
};
|
|
375
|
+
resolution: {
|
|
376
|
+
bugsResolved: number;
|
|
377
|
+
avgResolutionDays: number;
|
|
378
|
+
trend: 'up' | 'down' | 'stable';
|
|
379
|
+
changePercent?: number;
|
|
380
|
+
};
|
|
381
|
+
coverage: {
|
|
382
|
+
routeCoverage: number;
|
|
383
|
+
routesWithTests: number;
|
|
384
|
+
totalRoutes: number;
|
|
385
|
+
};
|
|
386
|
+
testerHealth: {
|
|
387
|
+
activeTesters: number;
|
|
388
|
+
totalTesters: number;
|
|
389
|
+
utilizationPercent: number;
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/** QA Health score with grade */
|
|
393
|
+
interface QAHealthScore {
|
|
394
|
+
score: number;
|
|
395
|
+
grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
396
|
+
breakdown: {
|
|
397
|
+
coverage: number;
|
|
398
|
+
velocity: number;
|
|
399
|
+
resolution: number;
|
|
400
|
+
stability: number;
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/** Aggregated route test statistics */
|
|
404
|
+
interface RouteTestStats {
|
|
405
|
+
id: string;
|
|
406
|
+
projectId: string;
|
|
407
|
+
route: string;
|
|
408
|
+
totalBugs: number;
|
|
409
|
+
openBugs: number;
|
|
410
|
+
criticalBugs: number;
|
|
411
|
+
highBugs: number;
|
|
412
|
+
resolvedBugs: number;
|
|
413
|
+
testCaseCount: number;
|
|
414
|
+
testsByTrack: Record<string, number>;
|
|
415
|
+
lastTestedAt: string | null;
|
|
416
|
+
totalTestExecutions: number;
|
|
417
|
+
passCount: number;
|
|
418
|
+
failCount: number;
|
|
419
|
+
passRate: number | null;
|
|
420
|
+
regressionCount: number;
|
|
421
|
+
lastRegressionAt: string | null;
|
|
422
|
+
bugsLast7Days: number;
|
|
423
|
+
bugsLast30Days: number;
|
|
424
|
+
priorityScore: number;
|
|
425
|
+
calculatedAt: string;
|
|
426
|
+
}
|
|
427
|
+
/** Coverage matrix cell */
|
|
428
|
+
interface CoverageMatrixCell {
|
|
429
|
+
testCount: number;
|
|
430
|
+
passCount: number;
|
|
431
|
+
failCount: number;
|
|
432
|
+
passRate: number | null;
|
|
433
|
+
lastTestedAt: string | null;
|
|
434
|
+
staleDays: number | null;
|
|
435
|
+
}
|
|
436
|
+
/** Coverage matrix row (route × tracks) */
|
|
437
|
+
interface CoverageMatrixRow {
|
|
438
|
+
route: string;
|
|
439
|
+
totalTests: number;
|
|
440
|
+
openBugs: number;
|
|
441
|
+
criticalBugs: number;
|
|
442
|
+
lastTestedAt: string | null;
|
|
443
|
+
tracks: Record<string, CoverageMatrixCell>;
|
|
444
|
+
}
|
|
266
445
|
|
|
267
446
|
/**
|
|
268
447
|
* BugBear Client
|
|
@@ -304,8 +483,29 @@ declare class BugBearClient {
|
|
|
304
483
|
/**
|
|
305
484
|
* Get current tester info
|
|
306
485
|
* Looks up tester by email from the host app's authenticated user
|
|
486
|
+
* Checks both primary email AND additional_emails array
|
|
487
|
+
* Uses parameterized RPC function to prevent SQL injection
|
|
307
488
|
*/
|
|
308
489
|
getTesterInfo(): Promise<TesterInfo | null>;
|
|
490
|
+
/**
|
|
491
|
+
* Basic email format validation (defense in depth)
|
|
492
|
+
*/
|
|
493
|
+
private isValidEmail;
|
|
494
|
+
/**
|
|
495
|
+
* Validate report input before submission
|
|
496
|
+
* Returns error message if invalid, null if valid
|
|
497
|
+
*/
|
|
498
|
+
private validateReport;
|
|
499
|
+
/**
|
|
500
|
+
* Validate profile update input
|
|
501
|
+
* Returns error message if invalid, null if valid
|
|
502
|
+
*/
|
|
503
|
+
private validateProfileUpdate;
|
|
504
|
+
/**
|
|
505
|
+
* Check rate limit for an action
|
|
506
|
+
* Returns { allowed: boolean, error?: string, remaining?: number }
|
|
507
|
+
*/
|
|
508
|
+
private checkRateLimit;
|
|
309
509
|
/**
|
|
310
510
|
* Update tester profile
|
|
311
511
|
* Allows testers to update their name, additional emails, avatar, and platforms
|
|
@@ -467,4 +667,4 @@ declare function captureError(error: Error, errorInfo?: {
|
|
|
467
667
|
componentStack?: string;
|
|
468
668
|
};
|
|
469
669
|
|
|
470
|
-
export { type AppContext, BugBearClient, type BugBearConfig, type BugBearReport, type BugBearTheme, type ChecklistResult, type ConsoleLogEntry, type DeviceInfo, type EnhancedBugContext, type HostUserInfo, type MessageSenderType, type NetworkRequest, type QATrack, type ReportStatus, type ReportType, type RubricMode, type RubricResult, type Severity, type TestAssignment, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, captureError, contextCapture, createBugBear };
|
|
670
|
+
export { type AppContext, BugBearClient, type BugBearConfig, type BugBearReport, type BugBearTheme, type ChecklistItem, type ChecklistResult, type ConsoleLogEntry, type CoverageGap, type CoverageMatrixCell, type CoverageMatrixRow, type DeployChecklist, type DeviceInfo, type EnhancedBugContext, type HostUserInfo, type MessageSenderType, type NetworkRequest, type PriorityFactors, type QAHealthMetrics, type QAHealthScore, type QATrack, type RegressionEvent, type ReportStatus, type ReportType, type RoutePriority, type RouteTestStats, type RubricMode, type RubricResult, type Severity, type TestAssignment, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, captureError, contextCapture, createBugBear };
|
package/dist/index.js
CHANGED
|
@@ -86,7 +86,16 @@ var BugBearClient = class {
|
|
|
86
86
|
*/
|
|
87
87
|
async submitReport(report) {
|
|
88
88
|
try {
|
|
89
|
+
const validationError = this.validateReport(report);
|
|
90
|
+
if (validationError) {
|
|
91
|
+
return { success: false, error: validationError };
|
|
92
|
+
}
|
|
89
93
|
const userInfo = await this.getCurrentUserInfo();
|
|
94
|
+
const rateLimitId = userInfo?.email || this.config.projectId;
|
|
95
|
+
const rateLimit = await this.checkRateLimit(rateLimitId, "report_submit");
|
|
96
|
+
if (!rateLimit.allowed) {
|
|
97
|
+
return { success: false, error: rateLimit.error };
|
|
98
|
+
}
|
|
90
99
|
if (!userInfo) {
|
|
91
100
|
console.error("BugBear: No user info available, cannot submit report");
|
|
92
101
|
return { success: false, error: "User not authenticated" };
|
|
@@ -192,44 +201,179 @@ var BugBearClient = class {
|
|
|
192
201
|
/**
|
|
193
202
|
* Get current tester info
|
|
194
203
|
* Looks up tester by email from the host app's authenticated user
|
|
204
|
+
* Checks both primary email AND additional_emails array
|
|
205
|
+
* Uses parameterized RPC function to prevent SQL injection
|
|
195
206
|
*/
|
|
196
207
|
async getTesterInfo() {
|
|
197
208
|
try {
|
|
198
209
|
const userInfo = await this.getCurrentUserInfo();
|
|
199
|
-
if (!userInfo) return null;
|
|
200
|
-
|
|
210
|
+
if (!userInfo?.email) return null;
|
|
211
|
+
if (!this.isValidEmail(userInfo.email)) {
|
|
212
|
+
console.warn("BugBear: Invalid email format");
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const { data, error } = await this.supabase.rpc("lookup_tester_by_email", {
|
|
216
|
+
p_project_id: this.config.projectId,
|
|
217
|
+
p_email: userInfo.email
|
|
218
|
+
}).maybeSingle();
|
|
201
219
|
if (error || !data) return null;
|
|
220
|
+
const tester = data;
|
|
202
221
|
return {
|
|
203
|
-
id:
|
|
204
|
-
name:
|
|
205
|
-
email:
|
|
206
|
-
additionalEmails:
|
|
207
|
-
avatarUrl:
|
|
208
|
-
platforms:
|
|
209
|
-
assignedTests:
|
|
210
|
-
completedTests:
|
|
222
|
+
id: tester.id,
|
|
223
|
+
name: tester.name,
|
|
224
|
+
email: tester.email,
|
|
225
|
+
additionalEmails: tester.additional_emails || [],
|
|
226
|
+
avatarUrl: tester.avatar_url || void 0,
|
|
227
|
+
platforms: tester.platforms || [],
|
|
228
|
+
assignedTests: tester.assigned_count || 0,
|
|
229
|
+
completedTests: tester.completed_count || 0
|
|
211
230
|
};
|
|
212
231
|
} catch (err) {
|
|
213
232
|
console.error("BugBear: getTesterInfo error", err);
|
|
214
233
|
return null;
|
|
215
234
|
}
|
|
216
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Basic email format validation (defense in depth)
|
|
238
|
+
*/
|
|
239
|
+
isValidEmail(email) {
|
|
240
|
+
if (!email || email.length > 254) return false;
|
|
241
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
242
|
+
return emailRegex.test(email);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Validate report input before submission
|
|
246
|
+
* Returns error message if invalid, null if valid
|
|
247
|
+
*/
|
|
248
|
+
validateReport(report) {
|
|
249
|
+
const validTypes = ["bug", "feedback", "suggestion", "test_pass", "test_fail"];
|
|
250
|
+
if (report.type && !validTypes.includes(report.type)) {
|
|
251
|
+
return `Invalid report type: ${report.type}. Must be one of: ${validTypes.join(", ")}`;
|
|
252
|
+
}
|
|
253
|
+
const validSeverities = ["critical", "high", "medium", "low"];
|
|
254
|
+
if (report.severity && !validSeverities.includes(report.severity)) {
|
|
255
|
+
return `Invalid severity: ${report.severity}. Must be one of: ${validSeverities.join(", ")}`;
|
|
256
|
+
}
|
|
257
|
+
if (report.title && report.title.length > 500) {
|
|
258
|
+
return "Title must be 500 characters or less";
|
|
259
|
+
}
|
|
260
|
+
if (report.description && report.description.length > 1e4) {
|
|
261
|
+
return "Description must be 10,000 characters or less";
|
|
262
|
+
}
|
|
263
|
+
if (report.screenshots && Array.isArray(report.screenshots)) {
|
|
264
|
+
if (report.screenshots.length > 10) {
|
|
265
|
+
return "Maximum 10 screenshots allowed";
|
|
266
|
+
}
|
|
267
|
+
for (const url of report.screenshots) {
|
|
268
|
+
if (typeof url !== "string" || url.length > 2e3) {
|
|
269
|
+
return "Invalid screenshot URL";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Validate profile update input
|
|
277
|
+
* Returns error message if invalid, null if valid
|
|
278
|
+
*/
|
|
279
|
+
validateProfileUpdate(updates) {
|
|
280
|
+
if (updates.name !== void 0) {
|
|
281
|
+
if (typeof updates.name !== "string" || updates.name.length > 100) {
|
|
282
|
+
return "Name must be 100 characters or less";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (updates.additionalEmails !== void 0) {
|
|
286
|
+
if (!Array.isArray(updates.additionalEmails)) {
|
|
287
|
+
return "Additional emails must be an array";
|
|
288
|
+
}
|
|
289
|
+
if (updates.additionalEmails.length > 5) {
|
|
290
|
+
return "Maximum 5 additional emails allowed";
|
|
291
|
+
}
|
|
292
|
+
for (const email of updates.additionalEmails) {
|
|
293
|
+
if (!this.isValidEmail(email)) {
|
|
294
|
+
return `Invalid email format: ${email}`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (updates.avatarUrl !== void 0 && updates.avatarUrl !== null) {
|
|
299
|
+
if (typeof updates.avatarUrl !== "string" || updates.avatarUrl.length > 2e3) {
|
|
300
|
+
return "Invalid avatar URL";
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (updates.platforms !== void 0) {
|
|
304
|
+
if (!Array.isArray(updates.platforms)) {
|
|
305
|
+
return "Platforms must be an array";
|
|
306
|
+
}
|
|
307
|
+
const validPlatforms = ["ios", "android", "web", "desktop", "other"];
|
|
308
|
+
for (const platform of updates.platforms) {
|
|
309
|
+
if (!validPlatforms.includes(platform)) {
|
|
310
|
+
return `Invalid platform: ${platform}. Must be one of: ${validPlatforms.join(", ")}`;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check rate limit for an action
|
|
318
|
+
* Returns { allowed: boolean, error?: string, remaining?: number }
|
|
319
|
+
*/
|
|
320
|
+
async checkRateLimit(identifier, action) {
|
|
321
|
+
try {
|
|
322
|
+
const { data, error } = await this.supabase.rpc("check_rate_limit", {
|
|
323
|
+
p_identifier: identifier,
|
|
324
|
+
p_action: action,
|
|
325
|
+
p_project_id: this.config.projectId
|
|
326
|
+
});
|
|
327
|
+
if (error) {
|
|
328
|
+
console.warn("BugBear: Rate limit check failed, allowing request", error.message);
|
|
329
|
+
return { allowed: true };
|
|
330
|
+
}
|
|
331
|
+
if (!data.allowed) {
|
|
332
|
+
return {
|
|
333
|
+
allowed: false,
|
|
334
|
+
error: `Rate limit exceeded. Try again in ${Math.ceil((new Date(data.reset_at).getTime() - Date.now()) / 1e3)} seconds.`,
|
|
335
|
+
remaining: 0,
|
|
336
|
+
resetAt: data.reset_at
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
allowed: true,
|
|
341
|
+
remaining: data.remaining,
|
|
342
|
+
resetAt: data.reset_at
|
|
343
|
+
};
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.warn("BugBear: Rate limit check error", err);
|
|
346
|
+
return { allowed: true };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
217
349
|
/**
|
|
218
350
|
* Update tester profile
|
|
219
351
|
* Allows testers to update their name, additional emails, avatar, and platforms
|
|
220
352
|
*/
|
|
221
353
|
async updateTesterProfile(updates) {
|
|
222
354
|
try {
|
|
355
|
+
const validationError = this.validateProfileUpdate(updates);
|
|
356
|
+
if (validationError) {
|
|
357
|
+
return { success: false, error: validationError };
|
|
358
|
+
}
|
|
223
359
|
const userInfo = await this.getCurrentUserInfo();
|
|
224
360
|
if (!userInfo) {
|
|
225
361
|
return { success: false, error: "Not authenticated" };
|
|
226
362
|
}
|
|
363
|
+
const rateLimit = await this.checkRateLimit(userInfo.email, "profile_update");
|
|
364
|
+
if (!rateLimit.allowed) {
|
|
365
|
+
return { success: false, error: rateLimit.error };
|
|
366
|
+
}
|
|
367
|
+
const testerInfo = await this.getTesterInfo();
|
|
368
|
+
if (!testerInfo) {
|
|
369
|
+
return { success: false, error: "Not a registered tester" };
|
|
370
|
+
}
|
|
227
371
|
const updateData = {};
|
|
228
372
|
if (updates.name !== void 0) updateData.name = updates.name;
|
|
229
373
|
if (updates.additionalEmails !== void 0) updateData.additional_emails = updates.additionalEmails;
|
|
230
374
|
if (updates.avatarUrl !== void 0) updateData.avatar_url = updates.avatarUrl;
|
|
231
375
|
if (updates.platforms !== void 0) updateData.platforms = updates.platforms;
|
|
232
|
-
const { error } = await this.supabase.from("testers").update(updateData).eq("
|
|
376
|
+
const { error } = await this.supabase.from("testers").update(updateData).eq("id", testerInfo.id);
|
|
233
377
|
if (error) {
|
|
234
378
|
console.error("BugBear: updateTesterProfile error", error);
|
|
235
379
|
return { success: false, error: error.message };
|
|
@@ -505,6 +649,11 @@ var BugBearClient = class {
|
|
|
505
649
|
console.error("BugBear: No tester info, cannot send message");
|
|
506
650
|
return false;
|
|
507
651
|
}
|
|
652
|
+
const rateLimit = await this.checkRateLimit(testerInfo.email, "message_send");
|
|
653
|
+
if (!rateLimit.allowed) {
|
|
654
|
+
console.error("BugBear: Rate limit exceeded for messages");
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
508
657
|
const { error } = await this.supabase.from("discussion_messages").insert({
|
|
509
658
|
thread_id: threadId,
|
|
510
659
|
sender_type: "tester",
|
package/dist/index.mjs
CHANGED
|
@@ -57,7 +57,16 @@ var BugBearClient = class {
|
|
|
57
57
|
*/
|
|
58
58
|
async submitReport(report) {
|
|
59
59
|
try {
|
|
60
|
+
const validationError = this.validateReport(report);
|
|
61
|
+
if (validationError) {
|
|
62
|
+
return { success: false, error: validationError };
|
|
63
|
+
}
|
|
60
64
|
const userInfo = await this.getCurrentUserInfo();
|
|
65
|
+
const rateLimitId = userInfo?.email || this.config.projectId;
|
|
66
|
+
const rateLimit = await this.checkRateLimit(rateLimitId, "report_submit");
|
|
67
|
+
if (!rateLimit.allowed) {
|
|
68
|
+
return { success: false, error: rateLimit.error };
|
|
69
|
+
}
|
|
61
70
|
if (!userInfo) {
|
|
62
71
|
console.error("BugBear: No user info available, cannot submit report");
|
|
63
72
|
return { success: false, error: "User not authenticated" };
|
|
@@ -163,44 +172,179 @@ var BugBearClient = class {
|
|
|
163
172
|
/**
|
|
164
173
|
* Get current tester info
|
|
165
174
|
* Looks up tester by email from the host app's authenticated user
|
|
175
|
+
* Checks both primary email AND additional_emails array
|
|
176
|
+
* Uses parameterized RPC function to prevent SQL injection
|
|
166
177
|
*/
|
|
167
178
|
async getTesterInfo() {
|
|
168
179
|
try {
|
|
169
180
|
const userInfo = await this.getCurrentUserInfo();
|
|
170
|
-
if (!userInfo) return null;
|
|
171
|
-
|
|
181
|
+
if (!userInfo?.email) return null;
|
|
182
|
+
if (!this.isValidEmail(userInfo.email)) {
|
|
183
|
+
console.warn("BugBear: Invalid email format");
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const { data, error } = await this.supabase.rpc("lookup_tester_by_email", {
|
|
187
|
+
p_project_id: this.config.projectId,
|
|
188
|
+
p_email: userInfo.email
|
|
189
|
+
}).maybeSingle();
|
|
172
190
|
if (error || !data) return null;
|
|
191
|
+
const tester = data;
|
|
173
192
|
return {
|
|
174
|
-
id:
|
|
175
|
-
name:
|
|
176
|
-
email:
|
|
177
|
-
additionalEmails:
|
|
178
|
-
avatarUrl:
|
|
179
|
-
platforms:
|
|
180
|
-
assignedTests:
|
|
181
|
-
completedTests:
|
|
193
|
+
id: tester.id,
|
|
194
|
+
name: tester.name,
|
|
195
|
+
email: tester.email,
|
|
196
|
+
additionalEmails: tester.additional_emails || [],
|
|
197
|
+
avatarUrl: tester.avatar_url || void 0,
|
|
198
|
+
platforms: tester.platforms || [],
|
|
199
|
+
assignedTests: tester.assigned_count || 0,
|
|
200
|
+
completedTests: tester.completed_count || 0
|
|
182
201
|
};
|
|
183
202
|
} catch (err) {
|
|
184
203
|
console.error("BugBear: getTesterInfo error", err);
|
|
185
204
|
return null;
|
|
186
205
|
}
|
|
187
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Basic email format validation (defense in depth)
|
|
209
|
+
*/
|
|
210
|
+
isValidEmail(email) {
|
|
211
|
+
if (!email || email.length > 254) return false;
|
|
212
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
213
|
+
return emailRegex.test(email);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Validate report input before submission
|
|
217
|
+
* Returns error message if invalid, null if valid
|
|
218
|
+
*/
|
|
219
|
+
validateReport(report) {
|
|
220
|
+
const validTypes = ["bug", "feedback", "suggestion", "test_pass", "test_fail"];
|
|
221
|
+
if (report.type && !validTypes.includes(report.type)) {
|
|
222
|
+
return `Invalid report type: ${report.type}. Must be one of: ${validTypes.join(", ")}`;
|
|
223
|
+
}
|
|
224
|
+
const validSeverities = ["critical", "high", "medium", "low"];
|
|
225
|
+
if (report.severity && !validSeverities.includes(report.severity)) {
|
|
226
|
+
return `Invalid severity: ${report.severity}. Must be one of: ${validSeverities.join(", ")}`;
|
|
227
|
+
}
|
|
228
|
+
if (report.title && report.title.length > 500) {
|
|
229
|
+
return "Title must be 500 characters or less";
|
|
230
|
+
}
|
|
231
|
+
if (report.description && report.description.length > 1e4) {
|
|
232
|
+
return "Description must be 10,000 characters or less";
|
|
233
|
+
}
|
|
234
|
+
if (report.screenshots && Array.isArray(report.screenshots)) {
|
|
235
|
+
if (report.screenshots.length > 10) {
|
|
236
|
+
return "Maximum 10 screenshots allowed";
|
|
237
|
+
}
|
|
238
|
+
for (const url of report.screenshots) {
|
|
239
|
+
if (typeof url !== "string" || url.length > 2e3) {
|
|
240
|
+
return "Invalid screenshot URL";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Validate profile update input
|
|
248
|
+
* Returns error message if invalid, null if valid
|
|
249
|
+
*/
|
|
250
|
+
validateProfileUpdate(updates) {
|
|
251
|
+
if (updates.name !== void 0) {
|
|
252
|
+
if (typeof updates.name !== "string" || updates.name.length > 100) {
|
|
253
|
+
return "Name must be 100 characters or less";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (updates.additionalEmails !== void 0) {
|
|
257
|
+
if (!Array.isArray(updates.additionalEmails)) {
|
|
258
|
+
return "Additional emails must be an array";
|
|
259
|
+
}
|
|
260
|
+
if (updates.additionalEmails.length > 5) {
|
|
261
|
+
return "Maximum 5 additional emails allowed";
|
|
262
|
+
}
|
|
263
|
+
for (const email of updates.additionalEmails) {
|
|
264
|
+
if (!this.isValidEmail(email)) {
|
|
265
|
+
return `Invalid email format: ${email}`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (updates.avatarUrl !== void 0 && updates.avatarUrl !== null) {
|
|
270
|
+
if (typeof updates.avatarUrl !== "string" || updates.avatarUrl.length > 2e3) {
|
|
271
|
+
return "Invalid avatar URL";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (updates.platforms !== void 0) {
|
|
275
|
+
if (!Array.isArray(updates.platforms)) {
|
|
276
|
+
return "Platforms must be an array";
|
|
277
|
+
}
|
|
278
|
+
const validPlatforms = ["ios", "android", "web", "desktop", "other"];
|
|
279
|
+
for (const platform of updates.platforms) {
|
|
280
|
+
if (!validPlatforms.includes(platform)) {
|
|
281
|
+
return `Invalid platform: ${platform}. Must be one of: ${validPlatforms.join(", ")}`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Check rate limit for an action
|
|
289
|
+
* Returns { allowed: boolean, error?: string, remaining?: number }
|
|
290
|
+
*/
|
|
291
|
+
async checkRateLimit(identifier, action) {
|
|
292
|
+
try {
|
|
293
|
+
const { data, error } = await this.supabase.rpc("check_rate_limit", {
|
|
294
|
+
p_identifier: identifier,
|
|
295
|
+
p_action: action,
|
|
296
|
+
p_project_id: this.config.projectId
|
|
297
|
+
});
|
|
298
|
+
if (error) {
|
|
299
|
+
console.warn("BugBear: Rate limit check failed, allowing request", error.message);
|
|
300
|
+
return { allowed: true };
|
|
301
|
+
}
|
|
302
|
+
if (!data.allowed) {
|
|
303
|
+
return {
|
|
304
|
+
allowed: false,
|
|
305
|
+
error: `Rate limit exceeded. Try again in ${Math.ceil((new Date(data.reset_at).getTime() - Date.now()) / 1e3)} seconds.`,
|
|
306
|
+
remaining: 0,
|
|
307
|
+
resetAt: data.reset_at
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
allowed: true,
|
|
312
|
+
remaining: data.remaining,
|
|
313
|
+
resetAt: data.reset_at
|
|
314
|
+
};
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.warn("BugBear: Rate limit check error", err);
|
|
317
|
+
return { allowed: true };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
188
320
|
/**
|
|
189
321
|
* Update tester profile
|
|
190
322
|
* Allows testers to update their name, additional emails, avatar, and platforms
|
|
191
323
|
*/
|
|
192
324
|
async updateTesterProfile(updates) {
|
|
193
325
|
try {
|
|
326
|
+
const validationError = this.validateProfileUpdate(updates);
|
|
327
|
+
if (validationError) {
|
|
328
|
+
return { success: false, error: validationError };
|
|
329
|
+
}
|
|
194
330
|
const userInfo = await this.getCurrentUserInfo();
|
|
195
331
|
if (!userInfo) {
|
|
196
332
|
return { success: false, error: "Not authenticated" };
|
|
197
333
|
}
|
|
334
|
+
const rateLimit = await this.checkRateLimit(userInfo.email, "profile_update");
|
|
335
|
+
if (!rateLimit.allowed) {
|
|
336
|
+
return { success: false, error: rateLimit.error };
|
|
337
|
+
}
|
|
338
|
+
const testerInfo = await this.getTesterInfo();
|
|
339
|
+
if (!testerInfo) {
|
|
340
|
+
return { success: false, error: "Not a registered tester" };
|
|
341
|
+
}
|
|
198
342
|
const updateData = {};
|
|
199
343
|
if (updates.name !== void 0) updateData.name = updates.name;
|
|
200
344
|
if (updates.additionalEmails !== void 0) updateData.additional_emails = updates.additionalEmails;
|
|
201
345
|
if (updates.avatarUrl !== void 0) updateData.avatar_url = updates.avatarUrl;
|
|
202
346
|
if (updates.platforms !== void 0) updateData.platforms = updates.platforms;
|
|
203
|
-
const { error } = await this.supabase.from("testers").update(updateData).eq("
|
|
347
|
+
const { error } = await this.supabase.from("testers").update(updateData).eq("id", testerInfo.id);
|
|
204
348
|
if (error) {
|
|
205
349
|
console.error("BugBear: updateTesterProfile error", error);
|
|
206
350
|
return { success: false, error: error.message };
|
|
@@ -476,6 +620,11 @@ var BugBearClient = class {
|
|
|
476
620
|
console.error("BugBear: No tester info, cannot send message");
|
|
477
621
|
return false;
|
|
478
622
|
}
|
|
623
|
+
const rateLimit = await this.checkRateLimit(testerInfo.email, "message_send");
|
|
624
|
+
if (!rateLimit.allowed) {
|
|
625
|
+
console.error("BugBear: Rate limit exceeded for messages");
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
479
628
|
const { error } = await this.supabase.from("discussion_messages").insert({
|
|
480
629
|
thread_id: threadId,
|
|
481
630
|
sender_type: "tester",
|