@bbearai/core 0.5.4 → 0.6.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 CHANGED
@@ -132,12 +132,39 @@ interface HostUserInfo {
132
132
  name?: string;
133
133
  }
134
134
  interface BugBearConfig {
135
- /** Your BugBear project ID */
136
- projectId: string;
137
- /** Supabase URL for the BugBear backend */
138
- supabaseUrl: string;
139
- /** Supabase anon key for the BugBear backend */
140
- supabaseAnonKey: string;
135
+ /**
136
+ * BugBear API key (recommended).
137
+ * When provided, the SDK resolves projectId, supabaseUrl, and supabaseAnonKey
138
+ * automatically from the BugBear API. This is the simplest config path —
139
+ * only one env var needed.
140
+ *
141
+ * Get yours at https://app.bugbear.ai/settings/projects
142
+ */
143
+ apiKey?: string;
144
+ /**
145
+ * Base URL for the BugBear dashboard API.
146
+ * Defaults to 'https://app.bugbear.ai'. Only change this for
147
+ * self-hosted or local development environments.
148
+ */
149
+ apiBaseUrl?: string;
150
+ /**
151
+ * Your BugBear project ID.
152
+ * Required when using the explicit config path (without apiKey).
153
+ * Automatically resolved when apiKey is provided.
154
+ */
155
+ projectId?: string;
156
+ /**
157
+ * Supabase URL for the BugBear backend.
158
+ * Required when using the explicit config path (without apiKey).
159
+ * Automatically resolved when apiKey is provided.
160
+ */
161
+ supabaseUrl?: string;
162
+ /**
163
+ * Supabase anon key for the BugBear backend.
164
+ * Required when using the explicit config path (without apiKey).
165
+ * Automatically resolved when apiKey is provided.
166
+ */
167
+ supabaseAnonKey?: string;
141
168
  /** Enable voice input */
142
169
  enableVoice?: boolean;
143
170
  /** Enable screenshot capture */
@@ -778,7 +805,38 @@ declare class BugBearClient {
778
805
  private _queue;
779
806
  /** Active Realtime channel references for cleanup. */
780
807
  private realtimeChannels;
808
+ /**
809
+ * Resolves when the client is ready to make requests.
810
+ * For the explicit config path (supabaseUrl + supabaseAnonKey), this is immediate.
811
+ * For the apiKey path, this resolves after credentials are fetched from /api/v1/config.
812
+ */
813
+ private pendingInit;
814
+ /** Whether the client has been successfully initialized. */
815
+ private initialized;
816
+ /** Initialization error, if any. */
817
+ private initError;
781
818
  constructor(config: BugBearConfig);
819
+ /** Whether the client is ready for requests. */
820
+ get isReady(): boolean;
821
+ /** Wait until the client is ready. Throws if initialization failed. */
822
+ ready(): Promise<void>;
823
+ /**
824
+ * Resolve Supabase credentials from a BugBear API key.
825
+ * Checks localStorage cache first, falls back to /api/v1/config.
826
+ */
827
+ private resolveFromApiKey;
828
+ /** Apply resolved credentials and create the Supabase client. */
829
+ private applyResolvedConfig;
830
+ /** Initialize offline queue if configured. Shared by both init paths. */
831
+ private initOfflineQueue;
832
+ /** Read cached config from localStorage if available and not expired. */
833
+ private readConfigCache;
834
+ /** Write resolved config to localStorage cache. */
835
+ private writeConfigCache;
836
+ /** Simple string hash for cache keys — avoids storing raw API keys. */
837
+ private hashKey;
838
+ /** Ensure the client is initialized before making requests. */
839
+ private ensureReady;
782
840
  /**
783
841
  * Access the offline queue (if enabled).
784
842
  * Use this to check queue.count, subscribe to changes, or trigger flush.
@@ -1173,6 +1231,7 @@ declare class ContextCaptureManager {
1173
1231
  private navigationHistory;
1174
1232
  private originalConsole;
1175
1233
  private originalFetch?;
1234
+ private fetchHost;
1176
1235
  private originalPushState?;
1177
1236
  private originalReplaceState?;
1178
1237
  private popstateHandler?;
package/dist/index.d.ts CHANGED
@@ -132,12 +132,39 @@ interface HostUserInfo {
132
132
  name?: string;
133
133
  }
134
134
  interface BugBearConfig {
135
- /** Your BugBear project ID */
136
- projectId: string;
137
- /** Supabase URL for the BugBear backend */
138
- supabaseUrl: string;
139
- /** Supabase anon key for the BugBear backend */
140
- supabaseAnonKey: string;
135
+ /**
136
+ * BugBear API key (recommended).
137
+ * When provided, the SDK resolves projectId, supabaseUrl, and supabaseAnonKey
138
+ * automatically from the BugBear API. This is the simplest config path —
139
+ * only one env var needed.
140
+ *
141
+ * Get yours at https://app.bugbear.ai/settings/projects
142
+ */
143
+ apiKey?: string;
144
+ /**
145
+ * Base URL for the BugBear dashboard API.
146
+ * Defaults to 'https://app.bugbear.ai'. Only change this for
147
+ * self-hosted or local development environments.
148
+ */
149
+ apiBaseUrl?: string;
150
+ /**
151
+ * Your BugBear project ID.
152
+ * Required when using the explicit config path (without apiKey).
153
+ * Automatically resolved when apiKey is provided.
154
+ */
155
+ projectId?: string;
156
+ /**
157
+ * Supabase URL for the BugBear backend.
158
+ * Required when using the explicit config path (without apiKey).
159
+ * Automatically resolved when apiKey is provided.
160
+ */
161
+ supabaseUrl?: string;
162
+ /**
163
+ * Supabase anon key for the BugBear backend.
164
+ * Required when using the explicit config path (without apiKey).
165
+ * Automatically resolved when apiKey is provided.
166
+ */
167
+ supabaseAnonKey?: string;
141
168
  /** Enable voice input */
142
169
  enableVoice?: boolean;
143
170
  /** Enable screenshot capture */
@@ -778,7 +805,38 @@ declare class BugBearClient {
778
805
  private _queue;
779
806
  /** Active Realtime channel references for cleanup. */
780
807
  private realtimeChannels;
808
+ /**
809
+ * Resolves when the client is ready to make requests.
810
+ * For the explicit config path (supabaseUrl + supabaseAnonKey), this is immediate.
811
+ * For the apiKey path, this resolves after credentials are fetched from /api/v1/config.
812
+ */
813
+ private pendingInit;
814
+ /** Whether the client has been successfully initialized. */
815
+ private initialized;
816
+ /** Initialization error, if any. */
817
+ private initError;
781
818
  constructor(config: BugBearConfig);
819
+ /** Whether the client is ready for requests. */
820
+ get isReady(): boolean;
821
+ /** Wait until the client is ready. Throws if initialization failed. */
822
+ ready(): Promise<void>;
823
+ /**
824
+ * Resolve Supabase credentials from a BugBear API key.
825
+ * Checks localStorage cache first, falls back to /api/v1/config.
826
+ */
827
+ private resolveFromApiKey;
828
+ /** Apply resolved credentials and create the Supabase client. */
829
+ private applyResolvedConfig;
830
+ /** Initialize offline queue if configured. Shared by both init paths. */
831
+ private initOfflineQueue;
832
+ /** Read cached config from localStorage if available and not expired. */
833
+ private readConfigCache;
834
+ /** Write resolved config to localStorage cache. */
835
+ private writeConfigCache;
836
+ /** Simple string hash for cache keys — avoids storing raw API keys. */
837
+ private hashKey;
838
+ /** Ensure the client is initialized before making requests. */
839
+ private ensureReady;
782
840
  /**
783
841
  * Access the offline queue (if enabled).
784
842
  * Use this to check queue.count, subscribe to changes, or trigger flush.
@@ -1173,6 +1231,7 @@ declare class ContextCaptureManager {
1173
1231
  private navigationHistory;
1174
1232
  private originalConsole;
1175
1233
  private originalFetch?;
1234
+ private fetchHost;
1176
1235
  private originalPushState?;
1177
1236
  private originalReplaceState?;
1178
1237
  private popstateHandler?;
package/dist/index.js CHANGED
@@ -52,6 +52,7 @@ var ContextCaptureManager = class {
52
52
  this.networkRequests = [];
53
53
  this.navigationHistory = [];
54
54
  this.originalConsole = {};
55
+ this.fetchHost = null;
55
56
  this.isCapturing = false;
56
57
  }
57
58
  /**
@@ -74,8 +75,9 @@ var ContextCaptureManager = class {
74
75
  if (this.originalConsole.warn) console.warn = this.originalConsole.warn;
75
76
  if (this.originalConsole.error) console.error = this.originalConsole.error;
76
77
  if (this.originalConsole.info) console.info = this.originalConsole.info;
77
- if (this.originalFetch && typeof window !== "undefined") {
78
- window.fetch = this.originalFetch;
78
+ if (this.originalFetch && this.fetchHost) {
79
+ this.fetchHost.fetch = this.originalFetch;
80
+ this.fetchHost = null;
79
81
  }
80
82
  if (typeof window !== "undefined" && typeof history !== "undefined") {
81
83
  if (this.originalPushState) {
@@ -184,15 +186,19 @@ var ContextCaptureManager = class {
184
186
  });
185
187
  }
186
188
  captureFetch() {
187
- if (typeof window === "undefined" || typeof fetch === "undefined" || typeof document === "undefined") return;
188
- this.originalFetch = window.fetch;
189
+ if (typeof fetch === "undefined") return;
190
+ const host = typeof window !== "undefined" && typeof window.fetch === "function" ? window : typeof globalThis !== "undefined" && typeof globalThis.fetch === "function" ? globalThis : null;
191
+ if (!host) return;
192
+ const canCloneResponse = typeof document !== "undefined";
193
+ this.fetchHost = host;
194
+ this.originalFetch = host.fetch;
189
195
  const self = this;
190
- window.fetch = async function(input, init) {
196
+ host.fetch = async function(input, init) {
191
197
  const startTime = Date.now();
192
198
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
193
199
  const method = init?.method || "GET";
194
200
  try {
195
- const response = await self.originalFetch.call(window, input, init);
201
+ const response = await self.originalFetch.call(host, input, init);
196
202
  const requestEntry = {
197
203
  method,
198
204
  url: url.slice(0, 200),
@@ -201,7 +207,7 @@ var ContextCaptureManager = class {
201
207
  duration: Date.now() - startTime,
202
208
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
203
209
  };
204
- if (response.status >= 400) {
210
+ if (canCloneResponse && response.status >= 400) {
205
211
  try {
206
212
  const cloned = response.clone();
207
213
  const body = await cloned.text();
@@ -449,6 +455,9 @@ var formatPgError = (e) => {
449
455
  const { message, code, details, hint } = e;
450
456
  return { message, code, details, hint };
451
457
  };
458
+ var DEFAULT_API_BASE_URL = "https://app.bugbear.ai";
459
+ var CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
460
+ var CONFIG_CACHE_PREFIX = "bugbear_config_";
452
461
  var BugBearClient = class {
453
462
  constructor(config) {
454
463
  this.navigationHistory = [];
@@ -458,23 +467,133 @@ var BugBearClient = class {
458
467
  /** Active Realtime channel references for cleanup. */
459
468
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
460
469
  this.realtimeChannels = [];
461
- if (!config.supabaseUrl) {
462
- throw new Error("BugBear: supabaseUrl is required. Get it from your BugBear project settings.");
463
- }
464
- if (!config.supabaseAnonKey) {
465
- throw new Error("BugBear: supabaseAnonKey is required. Get it from your BugBear project settings.");
466
- }
470
+ /** Whether the client has been successfully initialized. */
471
+ this.initialized = false;
472
+ /** Initialization error, if any. */
473
+ this.initError = null;
467
474
  this.config = config;
468
- this.supabase = (0, import_supabase_js.createClient)(config.supabaseUrl, config.supabaseAnonKey);
469
- if (config.offlineQueue?.enabled) {
475
+ if (config.apiKey) {
476
+ this.pendingInit = this.resolveFromApiKey(config.apiKey);
477
+ } else if (config.supabaseUrl && config.supabaseAnonKey) {
478
+ if (!config.projectId) {
479
+ throw new Error(
480
+ "BugBear: projectId is required when using explicit Supabase credentials. Tip: Use apiKey instead for simpler setup \u2014 it resolves everything automatically."
481
+ );
482
+ }
483
+ this.supabase = (0, import_supabase_js.createClient)(config.supabaseUrl, config.supabaseAnonKey);
484
+ this.initialized = true;
485
+ this.pendingInit = Promise.resolve();
486
+ this.initOfflineQueue();
487
+ } else {
488
+ throw new Error(
489
+ "BugBear: Missing configuration. Provide either:\n \u2022 apiKey (recommended) \u2014 resolves everything automatically\n \u2022 projectId + supabaseUrl + supabaseAnonKey \u2014 explicit credentials\n\nGet your API key at https://app.bugbear.ai/settings/projects"
490
+ );
491
+ }
492
+ }
493
+ /** Whether the client is ready for requests. */
494
+ get isReady() {
495
+ return this.initialized;
496
+ }
497
+ /** Wait until the client is ready. Throws if initialization failed. */
498
+ async ready() {
499
+ await this.pendingInit;
500
+ if (this.initError) throw this.initError;
501
+ }
502
+ /**
503
+ * Resolve Supabase credentials from a BugBear API key.
504
+ * Checks localStorage cache first, falls back to /api/v1/config.
505
+ */
506
+ async resolveFromApiKey(apiKey) {
507
+ try {
508
+ const cached = this.readConfigCache(apiKey);
509
+ if (cached) {
510
+ this.applyResolvedConfig(cached);
511
+ return;
512
+ }
513
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
514
+ const response = await fetch(`${baseUrl}/api/v1/config`, {
515
+ headers: { Authorization: `Bearer ${apiKey}` }
516
+ });
517
+ if (!response.ok) {
518
+ const body = await response.json().catch(() => ({}));
519
+ const message = body.error || `HTTP ${response.status}`;
520
+ throw new Error(
521
+ `BugBear: Invalid API key \u2014 ${message}. Get yours at https://app.bugbear.ai/settings/projects`
522
+ );
523
+ }
524
+ const data = await response.json();
525
+ this.writeConfigCache(apiKey, data);
526
+ this.applyResolvedConfig(data);
527
+ } catch (err) {
528
+ this.initError = err instanceof Error ? err : new Error(String(err));
529
+ this.config.onError?.(this.initError, { context: "apikey_resolution_failed" });
530
+ throw this.initError;
531
+ }
532
+ }
533
+ /** Apply resolved credentials and create the Supabase client. */
534
+ applyResolvedConfig(resolved) {
535
+ this.config = {
536
+ ...this.config,
537
+ projectId: resolved.projectId,
538
+ supabaseUrl: resolved.supabaseUrl,
539
+ supabaseAnonKey: resolved.supabaseAnonKey
540
+ };
541
+ this.supabase = (0, import_supabase_js.createClient)(resolved.supabaseUrl, resolved.supabaseAnonKey);
542
+ this.initialized = true;
543
+ this.initOfflineQueue();
544
+ }
545
+ /** Initialize offline queue if configured. Shared by both init paths. */
546
+ initOfflineQueue() {
547
+ if (this.config.offlineQueue?.enabled) {
470
548
  this._queue = new OfflineQueue({
471
549
  enabled: true,
472
- maxItems: config.offlineQueue.maxItems,
473
- maxRetries: config.offlineQueue.maxRetries
550
+ maxItems: this.config.offlineQueue.maxItems,
551
+ maxRetries: this.config.offlineQueue.maxRetries
474
552
  });
475
553
  this.registerQueueHandlers();
476
554
  }
477
555
  }
556
+ /** Read cached config from localStorage if available and not expired. */
557
+ readConfigCache(apiKey) {
558
+ if (typeof localStorage === "undefined") return null;
559
+ try {
560
+ const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
561
+ const raw = localStorage.getItem(key);
562
+ if (!raw) return null;
563
+ const cached = JSON.parse(raw);
564
+ if (Date.now() - cached.cachedAt > CONFIG_CACHE_TTL_MS) {
565
+ localStorage.removeItem(key);
566
+ return null;
567
+ }
568
+ return cached;
569
+ } catch {
570
+ return null;
571
+ }
572
+ }
573
+ /** Write resolved config to localStorage cache. */
574
+ writeConfigCache(apiKey, data) {
575
+ if (typeof localStorage === "undefined") return;
576
+ try {
577
+ const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
578
+ const cached = { ...data, cachedAt: Date.now() };
579
+ localStorage.setItem(key, JSON.stringify(cached));
580
+ } catch {
581
+ }
582
+ }
583
+ /** Simple string hash for cache keys — avoids storing raw API keys. */
584
+ hashKey(apiKey) {
585
+ let hash = 0;
586
+ for (let i = 0; i < apiKey.length; i++) {
587
+ hash = (hash << 5) - hash + apiKey.charCodeAt(i) | 0;
588
+ }
589
+ return hash.toString(36);
590
+ }
591
+ /** Ensure the client is initialized before making requests. */
592
+ async ensureReady() {
593
+ if (this.initialized) return;
594
+ await this.pendingInit;
595
+ if (this.initError) throw this.initError;
596
+ }
478
597
  // ── Offline Queue ─────────────────────────────────────────
479
598
  /**
480
599
  * Access the offline queue (if enabled).
@@ -630,6 +749,7 @@ var BugBearClient = class {
630
749
  * Get current user info from host app or BugBear's own auth
631
750
  */
632
751
  async getCurrentUserInfo() {
752
+ await this.ensureReady();
633
753
  if (this.config.getCurrentUser) {
634
754
  return await this.config.getCurrentUser();
635
755
  }
@@ -849,6 +969,7 @@ var BugBearClient = class {
849
969
  */
850
970
  async getAssignment(assignmentId) {
851
971
  try {
972
+ await this.ensureReady();
852
973
  const { data, error } = await this.supabase.from("test_assignments").select(`
853
974
  id,
854
975
  status,
@@ -920,6 +1041,7 @@ var BugBearClient = class {
920
1041
  */
921
1042
  async updateAssignmentStatus(assignmentId, status, options) {
922
1043
  try {
1044
+ await this.ensureReady();
923
1045
  const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
924
1046
  if (fetchError || !currentAssignment) {
925
1047
  console.error("BugBear: Assignment not found", {
@@ -1006,6 +1128,7 @@ var BugBearClient = class {
1006
1128
  */
1007
1129
  async reopenAssignment(assignmentId) {
1008
1130
  try {
1131
+ await this.ensureReady();
1009
1132
  const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
1010
1133
  if (fetchError || !current) {
1011
1134
  return { success: false, error: "Assignment not found" };
@@ -1045,6 +1168,7 @@ var BugBearClient = class {
1045
1168
  actualNotes = notes;
1046
1169
  }
1047
1170
  try {
1171
+ await this.ensureReady();
1048
1172
  const updateData = {
1049
1173
  status: "skipped",
1050
1174
  skip_reason: actualReason,
@@ -1214,6 +1338,7 @@ var BugBearClient = class {
1214
1338
  */
1215
1339
  async getTesterInfo() {
1216
1340
  try {
1341
+ await this.ensureReady();
1217
1342
  const userInfo = await this.getCurrentUserInfo();
1218
1343
  if (!userInfo?.email) return null;
1219
1344
  if (!this.isValidEmail(userInfo.email)) {
@@ -1505,6 +1630,7 @@ var BugBearClient = class {
1505
1630
  */
1506
1631
  async isQAEnabled() {
1507
1632
  try {
1633
+ await this.ensureReady();
1508
1634
  const { data, error } = await this.supabase.rpc("check_qa_enabled", {
1509
1635
  p_project_id: this.config.projectId
1510
1636
  });
@@ -1536,6 +1662,7 @@ var BugBearClient = class {
1536
1662
  */
1537
1663
  async uploadScreenshot(file, filename, bucket = "screenshots") {
1538
1664
  try {
1665
+ await this.ensureReady();
1539
1666
  const contentType = file.type || "image/png";
1540
1667
  const ext = contentType.includes("png") ? "png" : "jpg";
1541
1668
  const name = filename || `screenshot-${Date.now()}.${ext}`;
@@ -1576,6 +1703,7 @@ var BugBearClient = class {
1576
1703
  */
1577
1704
  async uploadImageFromUri(uri, filename, bucket = "screenshots") {
1578
1705
  try {
1706
+ await this.ensureReady();
1579
1707
  const response = await fetch(uri);
1580
1708
  const blob = await response.blob();
1581
1709
  const contentType = blob.type || "image/jpeg";
@@ -1670,6 +1798,7 @@ var BugBearClient = class {
1670
1798
  */
1671
1799
  async getFixRequests(options) {
1672
1800
  try {
1801
+ await this.ensureReady();
1673
1802
  let query = this.supabase.from("fix_requests").select("*").eq("project_id", this.config.projectId).order("created_at", { ascending: false }).limit(options?.limit || 20);
1674
1803
  if (options?.status) {
1675
1804
  query = query.eq("status", options.status);
@@ -1741,6 +1870,7 @@ var BugBearClient = class {
1741
1870
  */
1742
1871
  async getThreadMessages(threadId) {
1743
1872
  try {
1873
+ await this.ensureReady();
1744
1874
  const { data, error } = await this.supabase.from("discussion_messages").select(`
1745
1875
  id,
1746
1876
  thread_id,
@@ -1943,6 +2073,7 @@ var BugBearClient = class {
1943
2073
  */
1944
2074
  async endSession(sessionId, options = {}) {
1945
2075
  try {
2076
+ await this.ensureReady();
1946
2077
  const { data, error } = await this.supabase.rpc("end_qa_session", {
1947
2078
  p_session_id: sessionId,
1948
2079
  p_notes: options.notes || null,
@@ -1980,6 +2111,7 @@ var BugBearClient = class {
1980
2111
  */
1981
2112
  async getSession(sessionId) {
1982
2113
  try {
2114
+ await this.ensureReady();
1983
2115
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("id", sessionId).single();
1984
2116
  if (error || !data) return null;
1985
2117
  return this.transformSession(data);
@@ -2011,6 +2143,7 @@ var BugBearClient = class {
2011
2143
  */
2012
2144
  async addFinding(sessionId, options) {
2013
2145
  try {
2146
+ await this.ensureReady();
2014
2147
  const { data, error } = await this.supabase.rpc("add_session_finding", {
2015
2148
  p_session_id: sessionId,
2016
2149
  p_type: options.type,
@@ -2041,6 +2174,7 @@ var BugBearClient = class {
2041
2174
  */
2042
2175
  async getSessionFindings(sessionId) {
2043
2176
  try {
2177
+ await this.ensureReady();
2044
2178
  const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
2045
2179
  if (error) {
2046
2180
  console.error("BugBear: Failed to fetch findings", formatPgError(error));
@@ -2057,6 +2191,7 @@ var BugBearClient = class {
2057
2191
  */
2058
2192
  async convertFindingToBug(findingId) {
2059
2193
  try {
2194
+ await this.ensureReady();
2060
2195
  const { data, error } = await this.supabase.rpc("convert_finding_to_bug", {
2061
2196
  p_finding_id: findingId
2062
2197
  });
@@ -2076,6 +2211,7 @@ var BugBearClient = class {
2076
2211
  */
2077
2212
  async dismissFinding(findingId, reason) {
2078
2213
  try {
2214
+ await this.ensureReady();
2079
2215
  const { error } = await this.supabase.from("qa_findings").update({
2080
2216
  dismissed: true,
2081
2217
  dismissed_reason: reason || null,
package/dist/index.mjs CHANGED
@@ -18,6 +18,7 @@ var ContextCaptureManager = class {
18
18
  this.networkRequests = [];
19
19
  this.navigationHistory = [];
20
20
  this.originalConsole = {};
21
+ this.fetchHost = null;
21
22
  this.isCapturing = false;
22
23
  }
23
24
  /**
@@ -40,8 +41,9 @@ var ContextCaptureManager = class {
40
41
  if (this.originalConsole.warn) console.warn = this.originalConsole.warn;
41
42
  if (this.originalConsole.error) console.error = this.originalConsole.error;
42
43
  if (this.originalConsole.info) console.info = this.originalConsole.info;
43
- if (this.originalFetch && typeof window !== "undefined") {
44
- window.fetch = this.originalFetch;
44
+ if (this.originalFetch && this.fetchHost) {
45
+ this.fetchHost.fetch = this.originalFetch;
46
+ this.fetchHost = null;
45
47
  }
46
48
  if (typeof window !== "undefined" && typeof history !== "undefined") {
47
49
  if (this.originalPushState) {
@@ -150,15 +152,19 @@ var ContextCaptureManager = class {
150
152
  });
151
153
  }
152
154
  captureFetch() {
153
- if (typeof window === "undefined" || typeof fetch === "undefined" || typeof document === "undefined") return;
154
- this.originalFetch = window.fetch;
155
+ if (typeof fetch === "undefined") return;
156
+ const host = typeof window !== "undefined" && typeof window.fetch === "function" ? window : typeof globalThis !== "undefined" && typeof globalThis.fetch === "function" ? globalThis : null;
157
+ if (!host) return;
158
+ const canCloneResponse = typeof document !== "undefined";
159
+ this.fetchHost = host;
160
+ this.originalFetch = host.fetch;
155
161
  const self = this;
156
- window.fetch = async function(input, init) {
162
+ host.fetch = async function(input, init) {
157
163
  const startTime = Date.now();
158
164
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
159
165
  const method = init?.method || "GET";
160
166
  try {
161
- const response = await self.originalFetch.call(window, input, init);
167
+ const response = await self.originalFetch.call(host, input, init);
162
168
  const requestEntry = {
163
169
  method,
164
170
  url: url.slice(0, 200),
@@ -167,7 +173,7 @@ var ContextCaptureManager = class {
167
173
  duration: Date.now() - startTime,
168
174
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
169
175
  };
170
- if (response.status >= 400) {
176
+ if (canCloneResponse && response.status >= 400) {
171
177
  try {
172
178
  const cloned = response.clone();
173
179
  const body = await cloned.text();
@@ -415,6 +421,9 @@ var formatPgError = (e) => {
415
421
  const { message, code, details, hint } = e;
416
422
  return { message, code, details, hint };
417
423
  };
424
+ var DEFAULT_API_BASE_URL = "https://app.bugbear.ai";
425
+ var CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
426
+ var CONFIG_CACHE_PREFIX = "bugbear_config_";
418
427
  var BugBearClient = class {
419
428
  constructor(config) {
420
429
  this.navigationHistory = [];
@@ -424,23 +433,133 @@ var BugBearClient = class {
424
433
  /** Active Realtime channel references for cleanup. */
425
434
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
426
435
  this.realtimeChannels = [];
427
- if (!config.supabaseUrl) {
428
- throw new Error("BugBear: supabaseUrl is required. Get it from your BugBear project settings.");
429
- }
430
- if (!config.supabaseAnonKey) {
431
- throw new Error("BugBear: supabaseAnonKey is required. Get it from your BugBear project settings.");
432
- }
436
+ /** Whether the client has been successfully initialized. */
437
+ this.initialized = false;
438
+ /** Initialization error, if any. */
439
+ this.initError = null;
433
440
  this.config = config;
434
- this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey);
435
- if (config.offlineQueue?.enabled) {
441
+ if (config.apiKey) {
442
+ this.pendingInit = this.resolveFromApiKey(config.apiKey);
443
+ } else if (config.supabaseUrl && config.supabaseAnonKey) {
444
+ if (!config.projectId) {
445
+ throw new Error(
446
+ "BugBear: projectId is required when using explicit Supabase credentials. Tip: Use apiKey instead for simpler setup \u2014 it resolves everything automatically."
447
+ );
448
+ }
449
+ this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey);
450
+ this.initialized = true;
451
+ this.pendingInit = Promise.resolve();
452
+ this.initOfflineQueue();
453
+ } else {
454
+ throw new Error(
455
+ "BugBear: Missing configuration. Provide either:\n \u2022 apiKey (recommended) \u2014 resolves everything automatically\n \u2022 projectId + supabaseUrl + supabaseAnonKey \u2014 explicit credentials\n\nGet your API key at https://app.bugbear.ai/settings/projects"
456
+ );
457
+ }
458
+ }
459
+ /** Whether the client is ready for requests. */
460
+ get isReady() {
461
+ return this.initialized;
462
+ }
463
+ /** Wait until the client is ready. Throws if initialization failed. */
464
+ async ready() {
465
+ await this.pendingInit;
466
+ if (this.initError) throw this.initError;
467
+ }
468
+ /**
469
+ * Resolve Supabase credentials from a BugBear API key.
470
+ * Checks localStorage cache first, falls back to /api/v1/config.
471
+ */
472
+ async resolveFromApiKey(apiKey) {
473
+ try {
474
+ const cached = this.readConfigCache(apiKey);
475
+ if (cached) {
476
+ this.applyResolvedConfig(cached);
477
+ return;
478
+ }
479
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
480
+ const response = await fetch(`${baseUrl}/api/v1/config`, {
481
+ headers: { Authorization: `Bearer ${apiKey}` }
482
+ });
483
+ if (!response.ok) {
484
+ const body = await response.json().catch(() => ({}));
485
+ const message = body.error || `HTTP ${response.status}`;
486
+ throw new Error(
487
+ `BugBear: Invalid API key \u2014 ${message}. Get yours at https://app.bugbear.ai/settings/projects`
488
+ );
489
+ }
490
+ const data = await response.json();
491
+ this.writeConfigCache(apiKey, data);
492
+ this.applyResolvedConfig(data);
493
+ } catch (err) {
494
+ this.initError = err instanceof Error ? err : new Error(String(err));
495
+ this.config.onError?.(this.initError, { context: "apikey_resolution_failed" });
496
+ throw this.initError;
497
+ }
498
+ }
499
+ /** Apply resolved credentials and create the Supabase client. */
500
+ applyResolvedConfig(resolved) {
501
+ this.config = {
502
+ ...this.config,
503
+ projectId: resolved.projectId,
504
+ supabaseUrl: resolved.supabaseUrl,
505
+ supabaseAnonKey: resolved.supabaseAnonKey
506
+ };
507
+ this.supabase = createClient(resolved.supabaseUrl, resolved.supabaseAnonKey);
508
+ this.initialized = true;
509
+ this.initOfflineQueue();
510
+ }
511
+ /** Initialize offline queue if configured. Shared by both init paths. */
512
+ initOfflineQueue() {
513
+ if (this.config.offlineQueue?.enabled) {
436
514
  this._queue = new OfflineQueue({
437
515
  enabled: true,
438
- maxItems: config.offlineQueue.maxItems,
439
- maxRetries: config.offlineQueue.maxRetries
516
+ maxItems: this.config.offlineQueue.maxItems,
517
+ maxRetries: this.config.offlineQueue.maxRetries
440
518
  });
441
519
  this.registerQueueHandlers();
442
520
  }
443
521
  }
522
+ /** Read cached config from localStorage if available and not expired. */
523
+ readConfigCache(apiKey) {
524
+ if (typeof localStorage === "undefined") return null;
525
+ try {
526
+ const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
527
+ const raw = localStorage.getItem(key);
528
+ if (!raw) return null;
529
+ const cached = JSON.parse(raw);
530
+ if (Date.now() - cached.cachedAt > CONFIG_CACHE_TTL_MS) {
531
+ localStorage.removeItem(key);
532
+ return null;
533
+ }
534
+ return cached;
535
+ } catch {
536
+ return null;
537
+ }
538
+ }
539
+ /** Write resolved config to localStorage cache. */
540
+ writeConfigCache(apiKey, data) {
541
+ if (typeof localStorage === "undefined") return;
542
+ try {
543
+ const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
544
+ const cached = { ...data, cachedAt: Date.now() };
545
+ localStorage.setItem(key, JSON.stringify(cached));
546
+ } catch {
547
+ }
548
+ }
549
+ /** Simple string hash for cache keys — avoids storing raw API keys. */
550
+ hashKey(apiKey) {
551
+ let hash = 0;
552
+ for (let i = 0; i < apiKey.length; i++) {
553
+ hash = (hash << 5) - hash + apiKey.charCodeAt(i) | 0;
554
+ }
555
+ return hash.toString(36);
556
+ }
557
+ /** Ensure the client is initialized before making requests. */
558
+ async ensureReady() {
559
+ if (this.initialized) return;
560
+ await this.pendingInit;
561
+ if (this.initError) throw this.initError;
562
+ }
444
563
  // ── Offline Queue ─────────────────────────────────────────
445
564
  /**
446
565
  * Access the offline queue (if enabled).
@@ -596,6 +715,7 @@ var BugBearClient = class {
596
715
  * Get current user info from host app or BugBear's own auth
597
716
  */
598
717
  async getCurrentUserInfo() {
718
+ await this.ensureReady();
599
719
  if (this.config.getCurrentUser) {
600
720
  return await this.config.getCurrentUser();
601
721
  }
@@ -815,6 +935,7 @@ var BugBearClient = class {
815
935
  */
816
936
  async getAssignment(assignmentId) {
817
937
  try {
938
+ await this.ensureReady();
818
939
  const { data, error } = await this.supabase.from("test_assignments").select(`
819
940
  id,
820
941
  status,
@@ -886,6 +1007,7 @@ var BugBearClient = class {
886
1007
  */
887
1008
  async updateAssignmentStatus(assignmentId, status, options) {
888
1009
  try {
1010
+ await this.ensureReady();
889
1011
  const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
890
1012
  if (fetchError || !currentAssignment) {
891
1013
  console.error("BugBear: Assignment not found", {
@@ -972,6 +1094,7 @@ var BugBearClient = class {
972
1094
  */
973
1095
  async reopenAssignment(assignmentId) {
974
1096
  try {
1097
+ await this.ensureReady();
975
1098
  const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
976
1099
  if (fetchError || !current) {
977
1100
  return { success: false, error: "Assignment not found" };
@@ -1011,6 +1134,7 @@ var BugBearClient = class {
1011
1134
  actualNotes = notes;
1012
1135
  }
1013
1136
  try {
1137
+ await this.ensureReady();
1014
1138
  const updateData = {
1015
1139
  status: "skipped",
1016
1140
  skip_reason: actualReason,
@@ -1180,6 +1304,7 @@ var BugBearClient = class {
1180
1304
  */
1181
1305
  async getTesterInfo() {
1182
1306
  try {
1307
+ await this.ensureReady();
1183
1308
  const userInfo = await this.getCurrentUserInfo();
1184
1309
  if (!userInfo?.email) return null;
1185
1310
  if (!this.isValidEmail(userInfo.email)) {
@@ -1471,6 +1596,7 @@ var BugBearClient = class {
1471
1596
  */
1472
1597
  async isQAEnabled() {
1473
1598
  try {
1599
+ await this.ensureReady();
1474
1600
  const { data, error } = await this.supabase.rpc("check_qa_enabled", {
1475
1601
  p_project_id: this.config.projectId
1476
1602
  });
@@ -1502,6 +1628,7 @@ var BugBearClient = class {
1502
1628
  */
1503
1629
  async uploadScreenshot(file, filename, bucket = "screenshots") {
1504
1630
  try {
1631
+ await this.ensureReady();
1505
1632
  const contentType = file.type || "image/png";
1506
1633
  const ext = contentType.includes("png") ? "png" : "jpg";
1507
1634
  const name = filename || `screenshot-${Date.now()}.${ext}`;
@@ -1542,6 +1669,7 @@ var BugBearClient = class {
1542
1669
  */
1543
1670
  async uploadImageFromUri(uri, filename, bucket = "screenshots") {
1544
1671
  try {
1672
+ await this.ensureReady();
1545
1673
  const response = await fetch(uri);
1546
1674
  const blob = await response.blob();
1547
1675
  const contentType = blob.type || "image/jpeg";
@@ -1636,6 +1764,7 @@ var BugBearClient = class {
1636
1764
  */
1637
1765
  async getFixRequests(options) {
1638
1766
  try {
1767
+ await this.ensureReady();
1639
1768
  let query = this.supabase.from("fix_requests").select("*").eq("project_id", this.config.projectId).order("created_at", { ascending: false }).limit(options?.limit || 20);
1640
1769
  if (options?.status) {
1641
1770
  query = query.eq("status", options.status);
@@ -1707,6 +1836,7 @@ var BugBearClient = class {
1707
1836
  */
1708
1837
  async getThreadMessages(threadId) {
1709
1838
  try {
1839
+ await this.ensureReady();
1710
1840
  const { data, error } = await this.supabase.from("discussion_messages").select(`
1711
1841
  id,
1712
1842
  thread_id,
@@ -1909,6 +2039,7 @@ var BugBearClient = class {
1909
2039
  */
1910
2040
  async endSession(sessionId, options = {}) {
1911
2041
  try {
2042
+ await this.ensureReady();
1912
2043
  const { data, error } = await this.supabase.rpc("end_qa_session", {
1913
2044
  p_session_id: sessionId,
1914
2045
  p_notes: options.notes || null,
@@ -1946,6 +2077,7 @@ var BugBearClient = class {
1946
2077
  */
1947
2078
  async getSession(sessionId) {
1948
2079
  try {
2080
+ await this.ensureReady();
1949
2081
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("id", sessionId).single();
1950
2082
  if (error || !data) return null;
1951
2083
  return this.transformSession(data);
@@ -1977,6 +2109,7 @@ var BugBearClient = class {
1977
2109
  */
1978
2110
  async addFinding(sessionId, options) {
1979
2111
  try {
2112
+ await this.ensureReady();
1980
2113
  const { data, error } = await this.supabase.rpc("add_session_finding", {
1981
2114
  p_session_id: sessionId,
1982
2115
  p_type: options.type,
@@ -2007,6 +2140,7 @@ var BugBearClient = class {
2007
2140
  */
2008
2141
  async getSessionFindings(sessionId) {
2009
2142
  try {
2143
+ await this.ensureReady();
2010
2144
  const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
2011
2145
  if (error) {
2012
2146
  console.error("BugBear: Failed to fetch findings", formatPgError(error));
@@ -2023,6 +2157,7 @@ var BugBearClient = class {
2023
2157
  */
2024
2158
  async convertFindingToBug(findingId) {
2025
2159
  try {
2160
+ await this.ensureReady();
2026
2161
  const { data, error } = await this.supabase.rpc("convert_finding_to_bug", {
2027
2162
  p_finding_id: findingId
2028
2163
  });
@@ -2042,6 +2177,7 @@ var BugBearClient = class {
2042
2177
  */
2043
2178
  async dismissFinding(findingId, reason) {
2044
2179
  try {
2180
+ await this.ensureReady();
2045
2181
  const { error } = await this.supabase.from("qa_findings").update({
2046
2182
  dismissed: true,
2047
2183
  dismissed_reason: reason || null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/core",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Core utilities and types for BugBear QA platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",