@bbearai/core 0.7.1 → 0.7.2

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 CHANGED
@@ -789,6 +789,50 @@ interface TesterIssue {
789
789
  /** Original bug title (for reopened/test_fail issues) */
790
790
  originalBugTitle?: string;
791
791
  }
792
+ /** Delivery status for captured emails */
793
+ type EmailDeliveryStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'dropped' | 'deferred';
794
+ /** Email capture mode */
795
+ type EmailCaptureMode = 'capture' | 'intercept';
796
+ /** A captured email payload sent to the BugBear API */
797
+ interface EmailCapturePayload {
798
+ to: string[];
799
+ from?: string;
800
+ subject: string;
801
+ html?: string;
802
+ text?: string;
803
+ templateId?: string;
804
+ captureMode: EmailCaptureMode;
805
+ wasDelivered: boolean;
806
+ metadata?: Record<string, unknown>;
807
+ }
808
+ /** Result of capturing an email */
809
+ interface EmailCaptureResult {
810
+ success: boolean;
811
+ captureId?: string;
812
+ queued?: boolean;
813
+ error?: string;
814
+ }
815
+ /** Options for the email interceptor */
816
+ interface EmailInterceptorOptions {
817
+ /** 'capture' = log + send, 'intercept' = log + block */
818
+ mode: EmailCaptureMode;
819
+ /** Only capture emails sent to these addresses */
820
+ filterTo?: string[];
821
+ /** Replace real recipient addresses with "[redacted]" in captures */
822
+ redactRecipients?: boolean;
823
+ }
824
+ /** Extracted email fields from a send function call */
825
+ interface ExtractedEmailPayload {
826
+ to: string | string[];
827
+ from?: string;
828
+ subject: string;
829
+ html?: string;
830
+ text?: string;
831
+ templateId?: string;
832
+ [key: string]: unknown;
833
+ }
834
+ /** Custom extractor for non-standard email function signatures */
835
+ type EmailPayloadExtractor = (args: unknown[]) => ExtractedEmailPayload;
792
836
 
793
837
  /**
794
838
  * BugBear Offline Queue
@@ -815,7 +859,7 @@ declare class LocalStorageAdapter implements StorageAdapter {
815
859
  setItem(key: string, value: string): Promise<void>;
816
860
  removeItem(key: string): Promise<void>;
817
861
  }
818
- type QueueItemType = 'report' | 'message' | 'feedback';
862
+ type QueueItemType = 'report' | 'message' | 'feedback' | 'email_capture';
819
863
  interface QueueItem {
820
864
  /** Unique ID for this queued operation. */
821
865
  id: string;
@@ -1042,6 +1086,8 @@ declare class BugBearClient {
1042
1086
  private reportSubmitInFlight;
1043
1087
  /** Offline queue — only created when config.offlineQueue.enabled is true. */
1044
1088
  private _queue;
1089
+ /** Session cache storage — defaults to LocalStorageAdapter. */
1090
+ private _sessionStorage;
1045
1091
  /** Active Realtime channel references for cleanup. */
1046
1092
  private realtimeChannels;
1047
1093
  /** Error monitor instance — created when config.monitoring is present. */
@@ -1068,6 +1114,8 @@ declare class BugBearClient {
1068
1114
  private resolveFromApiKey;
1069
1115
  /** Apply resolved credentials and create the Supabase client. */
1070
1116
  private applyResolvedConfig;
1117
+ /** Cache key scoped to the active project. */
1118
+ private get sessionCacheKey();
1071
1119
  /** Initialize offline queue if configured. Shared by both init paths. */
1072
1120
  private initOfflineQueue;
1073
1121
  /** Initialize error monitoring if configured. Shared by both init paths. */
@@ -1080,6 +1128,12 @@ declare class BugBearClient {
1080
1128
  private hashKey;
1081
1129
  /** Ensure the client is initialized before making requests. */
1082
1130
  private ensureReady;
1131
+ /**
1132
+ * Fire-and-forget call to a dashboard notification endpoint.
1133
+ * Only works when apiKey is configured (needed for API auth).
1134
+ * Failures are silently ignored — notifications are best-effort.
1135
+ */
1136
+ private notifyDashboard;
1083
1137
  /**
1084
1138
  * Access the offline queue (if enabled).
1085
1139
  * Use this to check queue.count, subscribe to changes, or trigger flush.
@@ -1091,6 +1145,22 @@ declare class BugBearClient {
1091
1145
  * Web callers can skip this — LocalStorageAdapter is the default.
1092
1146
  */
1093
1147
  initQueue(storage?: StorageAdapter): Promise<void>;
1148
+ /**
1149
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
1150
+ * Must be called before getCachedSession() for persistence across app kills.
1151
+ * Web callers don't need this — LocalStorageAdapter is the default.
1152
+ */
1153
+ setSessionStorage(adapter: StorageAdapter): void;
1154
+ /**
1155
+ * Cache the active QA session locally for instant restore on app restart.
1156
+ * Pass null to clear the cache (e.g. after ending a session).
1157
+ */
1158
+ cacheSession(session: QASession | null): Promise<void>;
1159
+ /**
1160
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
1161
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
1162
+ */
1163
+ getCachedSession(): Promise<QASession | null>;
1094
1164
  private registerQueueHandlers;
1095
1165
  /** Whether realtime is enabled in config. */
1096
1166
  get realtimeEnabled(): boolean;
@@ -1137,6 +1207,11 @@ declare class BugBearClient {
1137
1207
  queued?: boolean;
1138
1208
  error?: string;
1139
1209
  }>;
1210
+ /**
1211
+ * Capture an email for QA testing.
1212
+ * Called by the email interceptor — not typically called directly.
1213
+ */
1214
+ captureEmail(payload: EmailCapturePayload): Promise<EmailCaptureResult>;
1140
1215
  /**
1141
1216
  * Get assigned tests for current user
1142
1217
  * First looks up the tester by email, then fetches their assignments
@@ -1253,6 +1328,14 @@ declare class BugBearClient {
1253
1328
  * reopened issues include original bug context.
1254
1329
  */
1255
1330
  getIssues(category: 'open' | 'done' | 'reopened'): Promise<TesterIssue[]>;
1331
+ /**
1332
+ * Reopen a done issue that the tester believes isn't actually fixed.
1333
+ * Transitions the report from a done status back to 'confirmed'.
1334
+ */
1335
+ reopenReport(reportId: string, reason: string): Promise<{
1336
+ success: boolean;
1337
+ error?: string;
1338
+ }>;
1256
1339
  /**
1257
1340
  * Basic email format validation (defense in depth)
1258
1341
  */
@@ -1558,4 +1641,26 @@ declare function captureError(error: Error, errorInfo?: {
1558
1641
  componentStack?: string;
1559
1642
  };
1560
1643
 
1561
- export { type AddFindingOptions, type AppContext, BUG_CATEGORIES, BugBearClient, type BugBearConfig, type BugBearMode, type BugBearReport, type BugBearTheme, type BugCategory, type ChecklistItem, type ChecklistResult, type ConsoleLogEntry, type CoverageGap, type CoverageMatrixCell, type CoverageMatrixRow, DedupWindow, type DeployChecklist, type DeviceInfo, type EndSessionOptions, type EnhancedBugContext, ErrorMonitor, type FindingSeverity, type FindingType, type HostUserInfo, type IssueCategory, type IssueCounts, LocalStorageAdapter, type MessageSenderType, type MonitorDeps, type MonitoringConfig, type MonitoringEvent, type NetworkRequest, OfflineQueue, type OfflineQueueConfig, type PriorityFactors, type ProjectRole, type QAFinding, type QAHealthMetrics, type QAHealthScore, type QASession, type QASessionStatus, type QATrack, type QueueItem, type QueueItemType, RNApiFailureHandler, RNCrashHandler, RNRageClickHandler, RageClickDetector, type RageClickEvent, type RegressionEvent, type ReportSource, type ReportStatus, type ReportType, type RoutePriority, type RouteTestStats, type RubricMode, type RubricResult, type Severity, type SkipReason, type StartSessionOptions, type StorageAdapter, type SubmitFeedbackOptions, type TestAssignment, type TestFeedback, type TestGroup, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterIssue, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, WebApiFailureHandler, WebCrashHandler, WebRageClickHandler, type WidgetColorScheme, type WidgetConfig, captureError, contextCapture, createBugBear, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
1644
+ interface WrapOptions {
1645
+ extractPayload?: EmailPayloadExtractor;
1646
+ }
1647
+ interface EmailInterceptor {
1648
+ wrap<T extends (...args: any[]) => Promise<any>>(sendFn: T, options?: WrapOptions): T;
1649
+ }
1650
+ /**
1651
+ * Create an email interceptor that captures emails sent through a wrapped function.
1652
+ *
1653
+ * Two modes:
1654
+ * - `capture`: emails are sent normally AND captured in BugBear (safe for production)
1655
+ * - `intercept`: emails are NOT sent, only captured in BugBear (for staging/QA)
1656
+ *
1657
+ * @example
1658
+ * ```ts
1659
+ * const interceptor = createEmailInterceptor(bugbearClient, { mode: 'intercept' });
1660
+ * const wrappedSend = interceptor.wrap(originalSendEmail);
1661
+ * await wrappedSend({ to: 'user@example.com', subject: 'Test', html: '<p>Hi</p>' });
1662
+ * ```
1663
+ */
1664
+ declare function createEmailInterceptor(client: BugBearClient, options?: EmailInterceptorOptions): EmailInterceptor;
1665
+
1666
+ export { type AddFindingOptions, type AppContext, BUG_CATEGORIES, BugBearClient, type BugBearConfig, type BugBearMode, type BugBearReport, type BugBearTheme, type BugCategory, type ChecklistItem, type ChecklistResult, type ConsoleLogEntry, type CoverageGap, type CoverageMatrixCell, type CoverageMatrixRow, DedupWindow, type DeployChecklist, type DeviceInfo, type EmailCaptureMode, type EmailCapturePayload, type EmailCaptureResult, type EmailDeliveryStatus, type EmailInterceptorOptions, type EmailPayloadExtractor, type EndSessionOptions, type EnhancedBugContext, ErrorMonitor, type ExtractedEmailPayload, type FindingSeverity, type FindingType, type HostUserInfo, type IssueCategory, type IssueCounts, LocalStorageAdapter, type MessageSenderType, type MonitorDeps, type MonitoringConfig, type MonitoringEvent, type NetworkRequest, OfflineQueue, type OfflineQueueConfig, type PriorityFactors, type ProjectRole, type QAFinding, type QAHealthMetrics, type QAHealthScore, type QASession, type QASessionStatus, type QATrack, type QueueItem, type QueueItemType, RNApiFailureHandler, RNCrashHandler, RNRageClickHandler, RageClickDetector, type RageClickEvent, type RegressionEvent, type ReportSource, type ReportStatus, type ReportType, type RoutePriority, type RouteTestStats, type RubricMode, type RubricResult, type Severity, type SkipReason, type StartSessionOptions, type StorageAdapter, type SubmitFeedbackOptions, type TestAssignment, type TestFeedback, type TestGroup, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterIssue, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, WebApiFailureHandler, WebCrashHandler, WebRageClickHandler, type WidgetColorScheme, type WidgetConfig, captureError, contextCapture, createBugBear, createEmailInterceptor, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
package/dist/index.d.ts CHANGED
@@ -789,6 +789,50 @@ interface TesterIssue {
789
789
  /** Original bug title (for reopened/test_fail issues) */
790
790
  originalBugTitle?: string;
791
791
  }
792
+ /** Delivery status for captured emails */
793
+ type EmailDeliveryStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'dropped' | 'deferred';
794
+ /** Email capture mode */
795
+ type EmailCaptureMode = 'capture' | 'intercept';
796
+ /** A captured email payload sent to the BugBear API */
797
+ interface EmailCapturePayload {
798
+ to: string[];
799
+ from?: string;
800
+ subject: string;
801
+ html?: string;
802
+ text?: string;
803
+ templateId?: string;
804
+ captureMode: EmailCaptureMode;
805
+ wasDelivered: boolean;
806
+ metadata?: Record<string, unknown>;
807
+ }
808
+ /** Result of capturing an email */
809
+ interface EmailCaptureResult {
810
+ success: boolean;
811
+ captureId?: string;
812
+ queued?: boolean;
813
+ error?: string;
814
+ }
815
+ /** Options for the email interceptor */
816
+ interface EmailInterceptorOptions {
817
+ /** 'capture' = log + send, 'intercept' = log + block */
818
+ mode: EmailCaptureMode;
819
+ /** Only capture emails sent to these addresses */
820
+ filterTo?: string[];
821
+ /** Replace real recipient addresses with "[redacted]" in captures */
822
+ redactRecipients?: boolean;
823
+ }
824
+ /** Extracted email fields from a send function call */
825
+ interface ExtractedEmailPayload {
826
+ to: string | string[];
827
+ from?: string;
828
+ subject: string;
829
+ html?: string;
830
+ text?: string;
831
+ templateId?: string;
832
+ [key: string]: unknown;
833
+ }
834
+ /** Custom extractor for non-standard email function signatures */
835
+ type EmailPayloadExtractor = (args: unknown[]) => ExtractedEmailPayload;
792
836
 
793
837
  /**
794
838
  * BugBear Offline Queue
@@ -815,7 +859,7 @@ declare class LocalStorageAdapter implements StorageAdapter {
815
859
  setItem(key: string, value: string): Promise<void>;
816
860
  removeItem(key: string): Promise<void>;
817
861
  }
818
- type QueueItemType = 'report' | 'message' | 'feedback';
862
+ type QueueItemType = 'report' | 'message' | 'feedback' | 'email_capture';
819
863
  interface QueueItem {
820
864
  /** Unique ID for this queued operation. */
821
865
  id: string;
@@ -1042,6 +1086,8 @@ declare class BugBearClient {
1042
1086
  private reportSubmitInFlight;
1043
1087
  /** Offline queue — only created when config.offlineQueue.enabled is true. */
1044
1088
  private _queue;
1089
+ /** Session cache storage — defaults to LocalStorageAdapter. */
1090
+ private _sessionStorage;
1045
1091
  /** Active Realtime channel references for cleanup. */
1046
1092
  private realtimeChannels;
1047
1093
  /** Error monitor instance — created when config.monitoring is present. */
@@ -1068,6 +1114,8 @@ declare class BugBearClient {
1068
1114
  private resolveFromApiKey;
1069
1115
  /** Apply resolved credentials and create the Supabase client. */
1070
1116
  private applyResolvedConfig;
1117
+ /** Cache key scoped to the active project. */
1118
+ private get sessionCacheKey();
1071
1119
  /** Initialize offline queue if configured. Shared by both init paths. */
1072
1120
  private initOfflineQueue;
1073
1121
  /** Initialize error monitoring if configured. Shared by both init paths. */
@@ -1080,6 +1128,12 @@ declare class BugBearClient {
1080
1128
  private hashKey;
1081
1129
  /** Ensure the client is initialized before making requests. */
1082
1130
  private ensureReady;
1131
+ /**
1132
+ * Fire-and-forget call to a dashboard notification endpoint.
1133
+ * Only works when apiKey is configured (needed for API auth).
1134
+ * Failures are silently ignored — notifications are best-effort.
1135
+ */
1136
+ private notifyDashboard;
1083
1137
  /**
1084
1138
  * Access the offline queue (if enabled).
1085
1139
  * Use this to check queue.count, subscribe to changes, or trigger flush.
@@ -1091,6 +1145,22 @@ declare class BugBearClient {
1091
1145
  * Web callers can skip this — LocalStorageAdapter is the default.
1092
1146
  */
1093
1147
  initQueue(storage?: StorageAdapter): Promise<void>;
1148
+ /**
1149
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
1150
+ * Must be called before getCachedSession() for persistence across app kills.
1151
+ * Web callers don't need this — LocalStorageAdapter is the default.
1152
+ */
1153
+ setSessionStorage(adapter: StorageAdapter): void;
1154
+ /**
1155
+ * Cache the active QA session locally for instant restore on app restart.
1156
+ * Pass null to clear the cache (e.g. after ending a session).
1157
+ */
1158
+ cacheSession(session: QASession | null): Promise<void>;
1159
+ /**
1160
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
1161
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
1162
+ */
1163
+ getCachedSession(): Promise<QASession | null>;
1094
1164
  private registerQueueHandlers;
1095
1165
  /** Whether realtime is enabled in config. */
1096
1166
  get realtimeEnabled(): boolean;
@@ -1137,6 +1207,11 @@ declare class BugBearClient {
1137
1207
  queued?: boolean;
1138
1208
  error?: string;
1139
1209
  }>;
1210
+ /**
1211
+ * Capture an email for QA testing.
1212
+ * Called by the email interceptor — not typically called directly.
1213
+ */
1214
+ captureEmail(payload: EmailCapturePayload): Promise<EmailCaptureResult>;
1140
1215
  /**
1141
1216
  * Get assigned tests for current user
1142
1217
  * First looks up the tester by email, then fetches their assignments
@@ -1253,6 +1328,14 @@ declare class BugBearClient {
1253
1328
  * reopened issues include original bug context.
1254
1329
  */
1255
1330
  getIssues(category: 'open' | 'done' | 'reopened'): Promise<TesterIssue[]>;
1331
+ /**
1332
+ * Reopen a done issue that the tester believes isn't actually fixed.
1333
+ * Transitions the report from a done status back to 'confirmed'.
1334
+ */
1335
+ reopenReport(reportId: string, reason: string): Promise<{
1336
+ success: boolean;
1337
+ error?: string;
1338
+ }>;
1256
1339
  /**
1257
1340
  * Basic email format validation (defense in depth)
1258
1341
  */
@@ -1558,4 +1641,26 @@ declare function captureError(error: Error, errorInfo?: {
1558
1641
  componentStack?: string;
1559
1642
  };
1560
1643
 
1561
- export { type AddFindingOptions, type AppContext, BUG_CATEGORIES, BugBearClient, type BugBearConfig, type BugBearMode, type BugBearReport, type BugBearTheme, type BugCategory, type ChecklistItem, type ChecklistResult, type ConsoleLogEntry, type CoverageGap, type CoverageMatrixCell, type CoverageMatrixRow, DedupWindow, type DeployChecklist, type DeviceInfo, type EndSessionOptions, type EnhancedBugContext, ErrorMonitor, type FindingSeverity, type FindingType, type HostUserInfo, type IssueCategory, type IssueCounts, LocalStorageAdapter, type MessageSenderType, type MonitorDeps, type MonitoringConfig, type MonitoringEvent, type NetworkRequest, OfflineQueue, type OfflineQueueConfig, type PriorityFactors, type ProjectRole, type QAFinding, type QAHealthMetrics, type QAHealthScore, type QASession, type QASessionStatus, type QATrack, type QueueItem, type QueueItemType, RNApiFailureHandler, RNCrashHandler, RNRageClickHandler, RageClickDetector, type RageClickEvent, type RegressionEvent, type ReportSource, type ReportStatus, type ReportType, type RoutePriority, type RouteTestStats, type RubricMode, type RubricResult, type Severity, type SkipReason, type StartSessionOptions, type StorageAdapter, type SubmitFeedbackOptions, type TestAssignment, type TestFeedback, type TestGroup, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterIssue, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, WebApiFailureHandler, WebCrashHandler, WebRageClickHandler, type WidgetColorScheme, type WidgetConfig, captureError, contextCapture, createBugBear, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
1644
+ interface WrapOptions {
1645
+ extractPayload?: EmailPayloadExtractor;
1646
+ }
1647
+ interface EmailInterceptor {
1648
+ wrap<T extends (...args: any[]) => Promise<any>>(sendFn: T, options?: WrapOptions): T;
1649
+ }
1650
+ /**
1651
+ * Create an email interceptor that captures emails sent through a wrapped function.
1652
+ *
1653
+ * Two modes:
1654
+ * - `capture`: emails are sent normally AND captured in BugBear (safe for production)
1655
+ * - `intercept`: emails are NOT sent, only captured in BugBear (for staging/QA)
1656
+ *
1657
+ * @example
1658
+ * ```ts
1659
+ * const interceptor = createEmailInterceptor(bugbearClient, { mode: 'intercept' });
1660
+ * const wrappedSend = interceptor.wrap(originalSendEmail);
1661
+ * await wrappedSend({ to: 'user@example.com', subject: 'Test', html: '<p>Hi</p>' });
1662
+ * ```
1663
+ */
1664
+ declare function createEmailInterceptor(client: BugBearClient, options?: EmailInterceptorOptions): EmailInterceptor;
1665
+
1666
+ export { type AddFindingOptions, type AppContext, BUG_CATEGORIES, BugBearClient, type BugBearConfig, type BugBearMode, type BugBearReport, type BugBearTheme, type BugCategory, type ChecklistItem, type ChecklistResult, type ConsoleLogEntry, type CoverageGap, type CoverageMatrixCell, type CoverageMatrixRow, DedupWindow, type DeployChecklist, type DeviceInfo, type EmailCaptureMode, type EmailCapturePayload, type EmailCaptureResult, type EmailDeliveryStatus, type EmailInterceptorOptions, type EmailPayloadExtractor, type EndSessionOptions, type EnhancedBugContext, ErrorMonitor, type ExtractedEmailPayload, type FindingSeverity, type FindingType, type HostUserInfo, type IssueCategory, type IssueCounts, LocalStorageAdapter, type MessageSenderType, type MonitorDeps, type MonitoringConfig, type MonitoringEvent, type NetworkRequest, OfflineQueue, type OfflineQueueConfig, type PriorityFactors, type ProjectRole, type QAFinding, type QAHealthMetrics, type QAHealthScore, type QASession, type QASessionStatus, type QATrack, type QueueItem, type QueueItemType, RNApiFailureHandler, RNCrashHandler, RNRageClickHandler, RageClickDetector, type RageClickEvent, type RegressionEvent, type ReportSource, type ReportStatus, type ReportType, type RoutePriority, type RouteTestStats, type RubricMode, type RubricResult, type Severity, type SkipReason, type StartSessionOptions, type StorageAdapter, type SubmitFeedbackOptions, type TestAssignment, type TestFeedback, type TestGroup, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterIssue, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, WebApiFailureHandler, WebCrashHandler, WebRageClickHandler, type WidgetColorScheme, type WidgetConfig, captureError, contextCapture, createBugBear, createEmailInterceptor, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  captureError: () => captureError,
37
37
  contextCapture: () => contextCapture,
38
38
  createBugBear: () => createBugBear,
39
+ createEmailInterceptor: () => createEmailInterceptor,
39
40
  generateFingerprint: () => generateFingerprint,
40
41
  isBugCategory: () => isBugCategory,
41
42
  isNetworkError: () => isNetworkError,
@@ -902,6 +903,8 @@ var BugBearClient = class {
902
903
  this.reportSubmitInFlight = false;
903
904
  /** Offline queue — only created when config.offlineQueue.enabled is true. */
904
905
  this._queue = null;
906
+ /** Session cache storage — defaults to LocalStorageAdapter. */
907
+ this._sessionStorage = new LocalStorageAdapter();
905
908
  /** Active Realtime channel references for cleanup. */
906
909
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
907
910
  this.realtimeChannels = [];
@@ -984,6 +987,10 @@ var BugBearClient = class {
984
987
  this.initOfflineQueue();
985
988
  this.initMonitoring();
986
989
  }
990
+ /** Cache key scoped to the active project. */
991
+ get sessionCacheKey() {
992
+ return `bugbear_session_${this.config.projectId ?? "unknown"}`;
993
+ }
987
994
  /** Initialize offline queue if configured. Shared by both init paths. */
988
995
  initOfflineQueue() {
989
996
  if (this.config.offlineQueue?.enabled) {
@@ -1047,6 +1054,26 @@ var BugBearClient = class {
1047
1054
  await this.pendingInit;
1048
1055
  if (this.initError) throw this.initError;
1049
1056
  }
1057
+ /**
1058
+ * Fire-and-forget call to a dashboard notification endpoint.
1059
+ * Only works when apiKey is configured (needed for API auth).
1060
+ * Failures are silently ignored — notifications are best-effort.
1061
+ */
1062
+ async notifyDashboard(path, body) {
1063
+ if (!this.config.apiKey) return;
1064
+ try {
1065
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
1066
+ await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
1067
+ method: "POST",
1068
+ headers: {
1069
+ "Content-Type": "application/json",
1070
+ "Authorization": `Bearer ${this.config.apiKey}`
1071
+ },
1072
+ body: JSON.stringify(body)
1073
+ });
1074
+ } catch {
1075
+ }
1076
+ }
1050
1077
  // ── Offline Queue ─────────────────────────────────────────
1051
1078
  /**
1052
1079
  * Access the offline queue (if enabled).
@@ -1073,6 +1100,48 @@ var BugBearClient = class {
1073
1100
  }
1074
1101
  await this._queue.load();
1075
1102
  }
1103
+ // ── Session Cache ──────────────────────────────────────────
1104
+ /**
1105
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
1106
+ * Must be called before getCachedSession() for persistence across app kills.
1107
+ * Web callers don't need this — LocalStorageAdapter is the default.
1108
+ */
1109
+ setSessionStorage(adapter) {
1110
+ this._sessionStorage = adapter;
1111
+ }
1112
+ /**
1113
+ * Cache the active QA session locally for instant restore on app restart.
1114
+ * Pass null to clear the cache (e.g. after ending a session).
1115
+ */
1116
+ async cacheSession(session) {
1117
+ try {
1118
+ if (session) {
1119
+ await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
1120
+ } else {
1121
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
1122
+ }
1123
+ } catch {
1124
+ }
1125
+ }
1126
+ /**
1127
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
1128
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
1129
+ */
1130
+ async getCachedSession() {
1131
+ try {
1132
+ const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
1133
+ if (!raw) return null;
1134
+ const session = JSON.parse(raw);
1135
+ const age = Date.now() - new Date(session.startedAt).getTime();
1136
+ if (age > 24 * 60 * 60 * 1e3) {
1137
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
1138
+ return null;
1139
+ }
1140
+ return session;
1141
+ } catch {
1142
+ return null;
1143
+ }
1144
+ }
1076
1145
  registerQueueHandlers() {
1077
1146
  if (!this._queue) return;
1078
1147
  this._queue.registerHandler("report", async (payload) => {
@@ -1090,6 +1159,11 @@ var BugBearClient = class {
1090
1159
  if (error) return { success: false, error: error.message };
1091
1160
  return { success: true };
1092
1161
  });
1162
+ this._queue.registerHandler("email_capture", async (payload) => {
1163
+ const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
1164
+ if (error) return { success: false, error: error.message };
1165
+ return { success: true };
1166
+ });
1093
1167
  }
1094
1168
  // ── Realtime Subscriptions ─────────────────────────────────
1095
1169
  /** Whether realtime is enabled in config. */
@@ -1277,6 +1351,8 @@ var BugBearClient = class {
1277
1351
  if (this.config.onReportSubmitted) {
1278
1352
  this.config.onReportSubmitted(report);
1279
1353
  }
1354
+ this.notifyDashboard("report", { reportId: data.id }).catch(() => {
1355
+ });
1280
1356
  return { success: true, reportId: data.id };
1281
1357
  } catch (err) {
1282
1358
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -1289,6 +1365,44 @@ var BugBearClient = class {
1289
1365
  this.reportSubmitInFlight = false;
1290
1366
  }
1291
1367
  }
1368
+ /**
1369
+ * Capture an email for QA testing.
1370
+ * Called by the email interceptor — not typically called directly.
1371
+ */
1372
+ async captureEmail(payload) {
1373
+ try {
1374
+ await this.ready();
1375
+ if (!payload.subject || !payload.to || payload.to.length === 0) {
1376
+ return { success: false, error: "subject and to are required" };
1377
+ }
1378
+ const record = {
1379
+ project_id: this.config.projectId,
1380
+ to_addresses: payload.to,
1381
+ from_address: payload.from || null,
1382
+ subject: payload.subject,
1383
+ html_content: payload.html || null,
1384
+ text_content: payload.text || null,
1385
+ template_id: payload.templateId || null,
1386
+ metadata: payload.metadata || {},
1387
+ capture_mode: payload.captureMode,
1388
+ was_delivered: payload.wasDelivered,
1389
+ delivery_status: payload.wasDelivered ? "sent" : "pending"
1390
+ };
1391
+ const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
1392
+ if (error) {
1393
+ if (this._queue && isNetworkError(error.message)) {
1394
+ await this._queue.enqueue("email_capture", record);
1395
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
1396
+ }
1397
+ console.error("BugBear: Failed to capture email", error.message);
1398
+ return { success: false, error: error.message };
1399
+ }
1400
+ return { success: true, captureId: data.id };
1401
+ } catch (err) {
1402
+ const message = err instanceof Error ? err.message : "Unknown error";
1403
+ return { success: false, error: message };
1404
+ }
1405
+ }
1292
1406
  /**
1293
1407
  * Get assigned tests for current user
1294
1408
  * First looks up the tester by email, then fetches their assignments
@@ -1910,6 +2024,32 @@ var BugBearClient = class {
1910
2024
  return [];
1911
2025
  }
1912
2026
  }
2027
+ /**
2028
+ * Reopen a done issue that the tester believes isn't actually fixed.
2029
+ * Transitions the report from a done status back to 'confirmed'.
2030
+ */
2031
+ async reopenReport(reportId, reason) {
2032
+ try {
2033
+ const testerInfo = await this.getTesterInfo();
2034
+ if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
2035
+ const { data, error } = await this.supabase.rpc("reopen_report", {
2036
+ p_report_id: reportId,
2037
+ p_tester_id: testerInfo.id,
2038
+ p_reason: reason
2039
+ });
2040
+ if (error) {
2041
+ console.error("BugBear: Failed to reopen report", formatPgError(error));
2042
+ return { success: false, error: error.message };
2043
+ }
2044
+ if (!data?.success) {
2045
+ return { success: false, error: data?.error || "Failed to reopen report" };
2046
+ }
2047
+ return { success: true };
2048
+ } catch (err) {
2049
+ console.error("BugBear: Error reopening report", err);
2050
+ return { success: false, error: "Unexpected error" };
2051
+ }
2052
+ }
1913
2053
  /**
1914
2054
  * Basic email format validation (defense in depth)
1915
2055
  */
@@ -2496,7 +2636,7 @@ var BugBearClient = class {
2496
2636
  insertData.attachments = safeAttachments;
2497
2637
  }
2498
2638
  }
2499
- const { error } = await this.supabase.from("discussion_messages").insert(insertData);
2639
+ const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
2500
2640
  if (error) {
2501
2641
  if (this._queue && isNetworkError(error.message)) {
2502
2642
  await this._queue.enqueue("message", insertData);
@@ -2505,6 +2645,10 @@ var BugBearClient = class {
2505
2645
  console.error("BugBear: Failed to send message", formatPgError(error));
2506
2646
  return false;
2507
2647
  }
2648
+ if (msgData?.id) {
2649
+ this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
2650
+ });
2651
+ }
2508
2652
  await this.markThreadAsRead(threadId);
2509
2653
  return true;
2510
2654
  } catch (err) {
@@ -2630,6 +2774,7 @@ var BugBearClient = class {
2630
2774
  if (!session) {
2631
2775
  return { success: false, error: "Session created but could not be fetched" };
2632
2776
  }
2777
+ await this.cacheSession(session);
2633
2778
  return { success: true, session };
2634
2779
  } catch (err) {
2635
2780
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -2653,6 +2798,7 @@ var BugBearClient = class {
2653
2798
  return { success: false, error: error.message };
2654
2799
  }
2655
2800
  const session = this.transformSession(data);
2801
+ await this.cacheSession(null);
2656
2802
  return { success: true, session };
2657
2803
  } catch (err) {
2658
2804
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -2668,8 +2814,13 @@ var BugBearClient = class {
2668
2814
  const testerInfo = await this.getTesterInfo();
2669
2815
  if (!testerInfo) return null;
2670
2816
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
2671
- if (error || !data) return null;
2672
- return this.transformSession(data);
2817
+ if (error || !data) {
2818
+ await this.cacheSession(null);
2819
+ return null;
2820
+ }
2821
+ const session = this.transformSession(data);
2822
+ await this.cacheSession(session);
2823
+ return session;
2673
2824
  } catch (err) {
2674
2825
  console.error("BugBear: Error fetching active session", err);
2675
2826
  return null;
@@ -2851,6 +3002,96 @@ var BugBearClient = class {
2851
3002
  function createBugBear(config) {
2852
3003
  return new BugBearClient(config);
2853
3004
  }
3005
+
3006
+ // src/email-interceptor.ts
3007
+ function defaultExtract(args) {
3008
+ const first = args[0];
3009
+ if (!first || typeof first !== "object") {
3010
+ throw new Error("BugBear email interceptor: first argument must be an object with { to, subject }");
3011
+ }
3012
+ const obj = first;
3013
+ return {
3014
+ to: obj.to,
3015
+ from: obj.from,
3016
+ subject: obj.subject,
3017
+ html: obj.html,
3018
+ text: obj.text,
3019
+ templateId: obj.templateId
3020
+ };
3021
+ }
3022
+ function normalizeToArray(to) {
3023
+ if (Array.isArray(to)) return to;
3024
+ return [to];
3025
+ }
3026
+ function createEmailInterceptor(client, options = { mode: "capture" }) {
3027
+ const { mode, filterTo, redactRecipients } = options;
3028
+ return {
3029
+ wrap(sendFn, wrapOpts) {
3030
+ const extract = wrapOpts?.extractPayload || defaultExtract;
3031
+ const wrapped = async (...args) => {
3032
+ let extracted;
3033
+ try {
3034
+ extracted = extract(args);
3035
+ } catch {
3036
+ return sendFn(...args);
3037
+ }
3038
+ const toArray = normalizeToArray(extracted.to);
3039
+ if (filterTo && filterTo.length > 0) {
3040
+ const matches = toArray.some(
3041
+ (addr) => filterTo.includes(addr.toLowerCase())
3042
+ );
3043
+ if (!matches) {
3044
+ return sendFn(...args);
3045
+ }
3046
+ }
3047
+ const capturedTo = redactRecipients ? toArray.map(() => "[redacted]") : toArray;
3048
+ if (mode === "intercept") {
3049
+ client.captureEmail({
3050
+ to: capturedTo,
3051
+ from: extracted.from,
3052
+ subject: extracted.subject,
3053
+ html: extracted.html,
3054
+ text: extracted.text,
3055
+ templateId: extracted.templateId,
3056
+ captureMode: "intercept",
3057
+ wasDelivered: false
3058
+ }).catch(() => {
3059
+ });
3060
+ return { success: true, intercepted: true };
3061
+ }
3062
+ try {
3063
+ const result = await sendFn(...args);
3064
+ client.captureEmail({
3065
+ to: capturedTo,
3066
+ from: extracted.from,
3067
+ subject: extracted.subject,
3068
+ html: extracted.html,
3069
+ text: extracted.text,
3070
+ templateId: extracted.templateId,
3071
+ captureMode: "capture",
3072
+ wasDelivered: true
3073
+ }).catch(() => {
3074
+ });
3075
+ return result;
3076
+ } catch (err) {
3077
+ client.captureEmail({
3078
+ to: capturedTo,
3079
+ from: extracted.from,
3080
+ subject: extracted.subject,
3081
+ html: extracted.html,
3082
+ text: extracted.text,
3083
+ templateId: extracted.templateId,
3084
+ captureMode: "capture",
3085
+ wasDelivered: false
3086
+ }).catch(() => {
3087
+ });
3088
+ throw err;
3089
+ }
3090
+ };
3091
+ return wrapped;
3092
+ }
3093
+ };
3094
+ }
2854
3095
  // Annotate the CommonJS export names for ESM import in node:
2855
3096
  0 && (module.exports = {
2856
3097
  BUG_CATEGORIES,
@@ -2869,6 +3110,7 @@ function createBugBear(config) {
2869
3110
  captureError,
2870
3111
  contextCapture,
2871
3112
  createBugBear,
3113
+ createEmailInterceptor,
2872
3114
  generateFingerprint,
2873
3115
  isBugCategory,
2874
3116
  isNetworkError,
package/dist/index.mjs CHANGED
@@ -857,6 +857,8 @@ var BugBearClient = class {
857
857
  this.reportSubmitInFlight = false;
858
858
  /** Offline queue — only created when config.offlineQueue.enabled is true. */
859
859
  this._queue = null;
860
+ /** Session cache storage — defaults to LocalStorageAdapter. */
861
+ this._sessionStorage = new LocalStorageAdapter();
860
862
  /** Active Realtime channel references for cleanup. */
861
863
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
862
864
  this.realtimeChannels = [];
@@ -939,6 +941,10 @@ var BugBearClient = class {
939
941
  this.initOfflineQueue();
940
942
  this.initMonitoring();
941
943
  }
944
+ /** Cache key scoped to the active project. */
945
+ get sessionCacheKey() {
946
+ return `bugbear_session_${this.config.projectId ?? "unknown"}`;
947
+ }
942
948
  /** Initialize offline queue if configured. Shared by both init paths. */
943
949
  initOfflineQueue() {
944
950
  if (this.config.offlineQueue?.enabled) {
@@ -1002,6 +1008,26 @@ var BugBearClient = class {
1002
1008
  await this.pendingInit;
1003
1009
  if (this.initError) throw this.initError;
1004
1010
  }
1011
+ /**
1012
+ * Fire-and-forget call to a dashboard notification endpoint.
1013
+ * Only works when apiKey is configured (needed for API auth).
1014
+ * Failures are silently ignored — notifications are best-effort.
1015
+ */
1016
+ async notifyDashboard(path, body) {
1017
+ if (!this.config.apiKey) return;
1018
+ try {
1019
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
1020
+ await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
1021
+ method: "POST",
1022
+ headers: {
1023
+ "Content-Type": "application/json",
1024
+ "Authorization": `Bearer ${this.config.apiKey}`
1025
+ },
1026
+ body: JSON.stringify(body)
1027
+ });
1028
+ } catch {
1029
+ }
1030
+ }
1005
1031
  // ── Offline Queue ─────────────────────────────────────────
1006
1032
  /**
1007
1033
  * Access the offline queue (if enabled).
@@ -1028,6 +1054,48 @@ var BugBearClient = class {
1028
1054
  }
1029
1055
  await this._queue.load();
1030
1056
  }
1057
+ // ── Session Cache ──────────────────────────────────────────
1058
+ /**
1059
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
1060
+ * Must be called before getCachedSession() for persistence across app kills.
1061
+ * Web callers don't need this — LocalStorageAdapter is the default.
1062
+ */
1063
+ setSessionStorage(adapter) {
1064
+ this._sessionStorage = adapter;
1065
+ }
1066
+ /**
1067
+ * Cache the active QA session locally for instant restore on app restart.
1068
+ * Pass null to clear the cache (e.g. after ending a session).
1069
+ */
1070
+ async cacheSession(session) {
1071
+ try {
1072
+ if (session) {
1073
+ await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
1074
+ } else {
1075
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
1076
+ }
1077
+ } catch {
1078
+ }
1079
+ }
1080
+ /**
1081
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
1082
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
1083
+ */
1084
+ async getCachedSession() {
1085
+ try {
1086
+ const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
1087
+ if (!raw) return null;
1088
+ const session = JSON.parse(raw);
1089
+ const age = Date.now() - new Date(session.startedAt).getTime();
1090
+ if (age > 24 * 60 * 60 * 1e3) {
1091
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
1092
+ return null;
1093
+ }
1094
+ return session;
1095
+ } catch {
1096
+ return null;
1097
+ }
1098
+ }
1031
1099
  registerQueueHandlers() {
1032
1100
  if (!this._queue) return;
1033
1101
  this._queue.registerHandler("report", async (payload) => {
@@ -1045,6 +1113,11 @@ var BugBearClient = class {
1045
1113
  if (error) return { success: false, error: error.message };
1046
1114
  return { success: true };
1047
1115
  });
1116
+ this._queue.registerHandler("email_capture", async (payload) => {
1117
+ const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
1118
+ if (error) return { success: false, error: error.message };
1119
+ return { success: true };
1120
+ });
1048
1121
  }
1049
1122
  // ── Realtime Subscriptions ─────────────────────────────────
1050
1123
  /** Whether realtime is enabled in config. */
@@ -1232,6 +1305,8 @@ var BugBearClient = class {
1232
1305
  if (this.config.onReportSubmitted) {
1233
1306
  this.config.onReportSubmitted(report);
1234
1307
  }
1308
+ this.notifyDashboard("report", { reportId: data.id }).catch(() => {
1309
+ });
1235
1310
  return { success: true, reportId: data.id };
1236
1311
  } catch (err) {
1237
1312
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -1244,6 +1319,44 @@ var BugBearClient = class {
1244
1319
  this.reportSubmitInFlight = false;
1245
1320
  }
1246
1321
  }
1322
+ /**
1323
+ * Capture an email for QA testing.
1324
+ * Called by the email interceptor — not typically called directly.
1325
+ */
1326
+ async captureEmail(payload) {
1327
+ try {
1328
+ await this.ready();
1329
+ if (!payload.subject || !payload.to || payload.to.length === 0) {
1330
+ return { success: false, error: "subject and to are required" };
1331
+ }
1332
+ const record = {
1333
+ project_id: this.config.projectId,
1334
+ to_addresses: payload.to,
1335
+ from_address: payload.from || null,
1336
+ subject: payload.subject,
1337
+ html_content: payload.html || null,
1338
+ text_content: payload.text || null,
1339
+ template_id: payload.templateId || null,
1340
+ metadata: payload.metadata || {},
1341
+ capture_mode: payload.captureMode,
1342
+ was_delivered: payload.wasDelivered,
1343
+ delivery_status: payload.wasDelivered ? "sent" : "pending"
1344
+ };
1345
+ const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
1346
+ if (error) {
1347
+ if (this._queue && isNetworkError(error.message)) {
1348
+ await this._queue.enqueue("email_capture", record);
1349
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
1350
+ }
1351
+ console.error("BugBear: Failed to capture email", error.message);
1352
+ return { success: false, error: error.message };
1353
+ }
1354
+ return { success: true, captureId: data.id };
1355
+ } catch (err) {
1356
+ const message = err instanceof Error ? err.message : "Unknown error";
1357
+ return { success: false, error: message };
1358
+ }
1359
+ }
1247
1360
  /**
1248
1361
  * Get assigned tests for current user
1249
1362
  * First looks up the tester by email, then fetches their assignments
@@ -1865,6 +1978,32 @@ var BugBearClient = class {
1865
1978
  return [];
1866
1979
  }
1867
1980
  }
1981
+ /**
1982
+ * Reopen a done issue that the tester believes isn't actually fixed.
1983
+ * Transitions the report from a done status back to 'confirmed'.
1984
+ */
1985
+ async reopenReport(reportId, reason) {
1986
+ try {
1987
+ const testerInfo = await this.getTesterInfo();
1988
+ if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
1989
+ const { data, error } = await this.supabase.rpc("reopen_report", {
1990
+ p_report_id: reportId,
1991
+ p_tester_id: testerInfo.id,
1992
+ p_reason: reason
1993
+ });
1994
+ if (error) {
1995
+ console.error("BugBear: Failed to reopen report", formatPgError(error));
1996
+ return { success: false, error: error.message };
1997
+ }
1998
+ if (!data?.success) {
1999
+ return { success: false, error: data?.error || "Failed to reopen report" };
2000
+ }
2001
+ return { success: true };
2002
+ } catch (err) {
2003
+ console.error("BugBear: Error reopening report", err);
2004
+ return { success: false, error: "Unexpected error" };
2005
+ }
2006
+ }
1868
2007
  /**
1869
2008
  * Basic email format validation (defense in depth)
1870
2009
  */
@@ -2451,7 +2590,7 @@ var BugBearClient = class {
2451
2590
  insertData.attachments = safeAttachments;
2452
2591
  }
2453
2592
  }
2454
- const { error } = await this.supabase.from("discussion_messages").insert(insertData);
2593
+ const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
2455
2594
  if (error) {
2456
2595
  if (this._queue && isNetworkError(error.message)) {
2457
2596
  await this._queue.enqueue("message", insertData);
@@ -2460,6 +2599,10 @@ var BugBearClient = class {
2460
2599
  console.error("BugBear: Failed to send message", formatPgError(error));
2461
2600
  return false;
2462
2601
  }
2602
+ if (msgData?.id) {
2603
+ this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
2604
+ });
2605
+ }
2463
2606
  await this.markThreadAsRead(threadId);
2464
2607
  return true;
2465
2608
  } catch (err) {
@@ -2585,6 +2728,7 @@ var BugBearClient = class {
2585
2728
  if (!session) {
2586
2729
  return { success: false, error: "Session created but could not be fetched" };
2587
2730
  }
2731
+ await this.cacheSession(session);
2588
2732
  return { success: true, session };
2589
2733
  } catch (err) {
2590
2734
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -2608,6 +2752,7 @@ var BugBearClient = class {
2608
2752
  return { success: false, error: error.message };
2609
2753
  }
2610
2754
  const session = this.transformSession(data);
2755
+ await this.cacheSession(null);
2611
2756
  return { success: true, session };
2612
2757
  } catch (err) {
2613
2758
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -2623,8 +2768,13 @@ var BugBearClient = class {
2623
2768
  const testerInfo = await this.getTesterInfo();
2624
2769
  if (!testerInfo) return null;
2625
2770
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
2626
- if (error || !data) return null;
2627
- return this.transformSession(data);
2771
+ if (error || !data) {
2772
+ await this.cacheSession(null);
2773
+ return null;
2774
+ }
2775
+ const session = this.transformSession(data);
2776
+ await this.cacheSession(session);
2777
+ return session;
2628
2778
  } catch (err) {
2629
2779
  console.error("BugBear: Error fetching active session", err);
2630
2780
  return null;
@@ -2806,6 +2956,96 @@ var BugBearClient = class {
2806
2956
  function createBugBear(config) {
2807
2957
  return new BugBearClient(config);
2808
2958
  }
2959
+
2960
+ // src/email-interceptor.ts
2961
+ function defaultExtract(args) {
2962
+ const first = args[0];
2963
+ if (!first || typeof first !== "object") {
2964
+ throw new Error("BugBear email interceptor: first argument must be an object with { to, subject }");
2965
+ }
2966
+ const obj = first;
2967
+ return {
2968
+ to: obj.to,
2969
+ from: obj.from,
2970
+ subject: obj.subject,
2971
+ html: obj.html,
2972
+ text: obj.text,
2973
+ templateId: obj.templateId
2974
+ };
2975
+ }
2976
+ function normalizeToArray(to) {
2977
+ if (Array.isArray(to)) return to;
2978
+ return [to];
2979
+ }
2980
+ function createEmailInterceptor(client, options = { mode: "capture" }) {
2981
+ const { mode, filterTo, redactRecipients } = options;
2982
+ return {
2983
+ wrap(sendFn, wrapOpts) {
2984
+ const extract = wrapOpts?.extractPayload || defaultExtract;
2985
+ const wrapped = async (...args) => {
2986
+ let extracted;
2987
+ try {
2988
+ extracted = extract(args);
2989
+ } catch {
2990
+ return sendFn(...args);
2991
+ }
2992
+ const toArray = normalizeToArray(extracted.to);
2993
+ if (filterTo && filterTo.length > 0) {
2994
+ const matches = toArray.some(
2995
+ (addr) => filterTo.includes(addr.toLowerCase())
2996
+ );
2997
+ if (!matches) {
2998
+ return sendFn(...args);
2999
+ }
3000
+ }
3001
+ const capturedTo = redactRecipients ? toArray.map(() => "[redacted]") : toArray;
3002
+ if (mode === "intercept") {
3003
+ client.captureEmail({
3004
+ to: capturedTo,
3005
+ from: extracted.from,
3006
+ subject: extracted.subject,
3007
+ html: extracted.html,
3008
+ text: extracted.text,
3009
+ templateId: extracted.templateId,
3010
+ captureMode: "intercept",
3011
+ wasDelivered: false
3012
+ }).catch(() => {
3013
+ });
3014
+ return { success: true, intercepted: true };
3015
+ }
3016
+ try {
3017
+ const result = await sendFn(...args);
3018
+ client.captureEmail({
3019
+ to: capturedTo,
3020
+ from: extracted.from,
3021
+ subject: extracted.subject,
3022
+ html: extracted.html,
3023
+ text: extracted.text,
3024
+ templateId: extracted.templateId,
3025
+ captureMode: "capture",
3026
+ wasDelivered: true
3027
+ }).catch(() => {
3028
+ });
3029
+ return result;
3030
+ } catch (err) {
3031
+ client.captureEmail({
3032
+ to: capturedTo,
3033
+ from: extracted.from,
3034
+ subject: extracted.subject,
3035
+ html: extracted.html,
3036
+ text: extracted.text,
3037
+ templateId: extracted.templateId,
3038
+ captureMode: "capture",
3039
+ wasDelivered: false
3040
+ }).catch(() => {
3041
+ });
3042
+ throw err;
3043
+ }
3044
+ };
3045
+ return wrapped;
3046
+ }
3047
+ };
3048
+ }
2809
3049
  export {
2810
3050
  BUG_CATEGORIES,
2811
3051
  BugBearClient,
@@ -2823,6 +3063,7 @@ export {
2823
3063
  captureError,
2824
3064
  contextCapture,
2825
3065
  createBugBear,
3066
+ createEmailInterceptor,
2826
3067
  generateFingerprint,
2827
3068
  isBugCategory,
2828
3069
  isNetworkError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/core",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Core utilities and types for BugBear QA platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",