lexmount 0.5.4 → 0.5.5

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.js CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  APIError: () => APIError,
34
34
  AuthenticationError: () => AuthenticationError,
35
+ CDPClient: () => CDPClient,
35
36
  ContextInfo: () => ContextInfo,
36
37
  ContextListResponse: () => ContextListResponse,
37
38
  ContextLockedError: () => ContextLockedError,
@@ -56,18 +57,20 @@ __export(index_exports, {
56
57
  TimeoutError: () => TimeoutError,
57
58
  VERSION: () => VERSION,
58
59
  ValidationError: () => ValidationError,
60
+ connectOverCDP: () => connectOverCDP,
59
61
  default: () => Lexmount,
62
+ deregisterIntegratedAuthCallback: () => deregisterIntegratedAuthCallback,
60
63
  disableLogging: () => disableLogging,
61
64
  enableLogging: () => enableLogging,
62
65
  getLogger: () => getLogger,
66
+ registerIntegratedAuthCallback: () => registerIntegratedAuthCallback,
63
67
  setLogLevel: () => setLogLevel
64
68
  });
65
69
  module.exports = __toCommonJS(index_exports);
66
70
 
67
- // src/client.ts
68
- var import_axios = require("axios");
69
- var import_axios2 = __toESM(require("axios"));
70
- var dotenv = __toESM(require("dotenv"));
71
+ // src/cdp.ts
72
+ var import_events = require("events");
73
+ var import_chrome_remote_interface = __toESM(require("chrome-remote-interface"));
71
74
 
72
75
  // src/errors.ts
73
76
  var LexmountError = class extends Error {
@@ -200,1034 +203,1262 @@ function enableLogging(level = "INFO") {
200
203
  setLogLevel(level);
201
204
  }
202
205
 
203
- // src/contexts.ts
206
+ // src/cdp.ts
207
+ function hasExplicitPort(url) {
208
+ const authority = url.match(/^[a-z][a-z\d+\-.]*:\/\/([^/?#]+)/i)?.[1] ?? "";
209
+ const host = authority.includes("@") ? authority.slice(authority.lastIndexOf("@") + 1) : authority;
210
+ if (host.startsWith("[")) {
211
+ return /\]:\d+$/.test(host);
212
+ }
213
+ return /:\d+$/.test(host);
214
+ }
215
+ function getDefaultPort(url) {
216
+ const protocol = getProtocol(url);
217
+ if (protocol === "ws") {
218
+ return 80;
219
+ }
220
+ if (protocol === "wss") {
221
+ return 443;
222
+ }
223
+ return void 0;
224
+ }
225
+ function getProtocol(url) {
226
+ return url.match(/^([a-z][a-z\d+\-.]*):\/\//i)?.[1]?.toLowerCase();
227
+ }
228
+ var CDPClient = class extends import_events.EventEmitter {
229
+ constructor(session) {
230
+ super();
231
+ this.closed = false;
232
+ const connectUrl = session.connectUrl;
233
+ if (!connectUrl) {
234
+ throw new ValidationError(`Session ${session.sessionId} does not include a CDP connect URL`);
235
+ }
236
+ this.session = session;
237
+ }
238
+ /**
239
+ * Connect the CDP client to the session.
240
+ */
241
+ async connect() {
242
+ if (this.rawClient && !this.closed) {
243
+ return this;
244
+ }
245
+ this.closed = false;
246
+ getLogger().debug(`Connecting to session ${this.session.sessionId} over CDP`);
247
+ const options = {
248
+ target: this.session.connectUrl,
249
+ local: true
250
+ };
251
+ const defaultPort = getDefaultPort(this.session.connectUrl);
252
+ if (!hasExplicitPort(this.session.connectUrl) && defaultPort) {
253
+ options.port = defaultPort;
254
+ }
255
+ if (getProtocol(this.session.connectUrl) === "wss") {
256
+ options.secure = true;
257
+ }
258
+ this.rawClient = await (0, import_chrome_remote_interface.default)(options);
259
+ this.rawClient.on("event", (message) => {
260
+ this.emit(message.method, message.params, message.sessionId);
261
+ });
262
+ this.rawClient.on("error", (err) => {
263
+ this.emit("error", err);
264
+ });
265
+ this.rawClient.on("disconnect", () => {
266
+ void this.close().catch((err) => {
267
+ getLogger().error(`Failed to close CDP client for session ${this.session.sessionId} after disconnect`, err);
268
+ });
269
+ this.emit("disconnect");
270
+ });
271
+ return this;
272
+ }
273
+ /**
274
+ * Send a raw Chrome DevTools Protocol command.
275
+ *
276
+ * Returns `undefined` if called before the client has connected.
277
+ */
278
+ async send(method, params, sessionId) {
279
+ if (this.closed) {
280
+ throw new ValidationError("Cannot send CDP command after client is closed");
281
+ }
282
+ const rawClient = this.rawClient;
283
+ if (!rawClient) {
284
+ return void 0;
285
+ }
286
+ return rawClient.send(method, params, sessionId);
287
+ }
288
+ /**
289
+ * Close the CDP connection and release internal listeners.
290
+ */
291
+ async close() {
292
+ if (this.closed) {
293
+ return;
294
+ }
295
+ this.closed = true;
296
+ if (this.rawClient) {
297
+ this.rawClient.removeAllListeners("event");
298
+ this.rawClient.removeAllListeners("disconnect");
299
+ this.rawClient.removeAllListeners("error");
300
+ await this.rawClient.close();
301
+ }
302
+ }
303
+ };
304
+ async function connectOverCDP(session) {
305
+ const client = new CDPClient(session);
306
+ await client.connect();
307
+ return client;
308
+ }
309
+
310
+ // src/sessions.ts
204
311
  function asRecord(value) {
205
312
  return typeof value === "object" && value !== null ? value : {};
206
313
  }
207
314
  function getString(value) {
208
315
  return typeof value === "string" ? value : void 0;
209
316
  }
210
- function parseTimestamp(value) {
211
- if (typeof value === "string" && value.length > 0) {
212
- return value;
213
- }
214
- if (typeof value === "number" && Number.isFinite(value)) {
215
- return new Date(value).toISOString();
216
- }
217
- return null;
317
+ function getNumber(value) {
318
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
218
319
  }
219
- function normalizeContextStatus(value) {
220
- if (value === true || value === "locked") {
221
- return "locked";
222
- }
223
- if (value === false || value === "available") {
224
- return "available";
320
+ function normalizeContextMode(mode) {
321
+ if (mode === "readWrite" || mode === "read_write") {
322
+ return "read_write";
225
323
  }
226
- return typeof value === "string" ? value : "available";
324
+ return "read_only";
227
325
  }
228
- var ContextInfo = class {
326
+ function delay(ms) {
327
+ return new Promise((resolve) => {
328
+ setTimeout(resolve, ms);
329
+ });
330
+ }
331
+ var PaginationInfo = class {
332
+ constructor(shape) {
333
+ this.currentPage = shape.currentPage;
334
+ this.pageSize = shape.pageSize;
335
+ this.totalCount = shape.totalCount;
336
+ this.totalPages = shape.totalPages;
337
+ this.activeCount = shape.activeCount;
338
+ this.closedCount = shape.closedCount;
339
+ }
340
+ };
341
+ var SessionInfo = class {
342
+ /** @internal */
229
343
  constructor(options) {
344
+ this.closed = false;
230
345
  this.id = options.id;
346
+ this.sessionId = options.id;
231
347
  this.status = options.status;
232
- this.metadata = options.metadata ?? {};
233
- this.createdAt = options.createdAt ?? null;
234
- this.updatedAt = options.updatedAt ?? null;
348
+ this.apiKey = options.apiKey;
349
+ this.projectId = options.projectId;
350
+ this.regionId = options.regionId ?? null;
351
+ this.region_id = this.regionId;
352
+ this.browserType = options.browserType;
353
+ this.createdAt = options.createdAt;
354
+ this.inspectUrl = options.inspectUrl;
355
+ this.containerId = options.containerId;
356
+ this.ws = options.ws ?? null;
357
+ this.client = options.client;
358
+ }
359
+ /**
360
+ * Playwright/CDP connection URL.
361
+ */
362
+ get connectUrl() {
363
+ return this.ws ?? "";
235
364
  }
365
+ /**
366
+ * Parsed session creation time.
367
+ */
236
368
  get createdAtDate() {
237
- if (!this.createdAt) {
238
- return null;
239
- }
240
369
  const date = new Date(this.createdAt);
241
370
  return Number.isNaN(date.getTime()) ? null : date;
242
371
  }
243
- get updatedAtDate() {
244
- if (!this.updatedAt) {
245
- return null;
372
+ /**
373
+ * Close the current session.
374
+ *
375
+ * This method is idempotent. Errors are logged and swallowed to keep cleanup safe.
376
+ */
377
+ async close() {
378
+ if (this.closed) {
379
+ getLogger().debug(`Session ${this.sessionId} is already closed.`);
380
+ return;
381
+ }
382
+ if (!this.client) {
383
+ getLogger().warn(
384
+ `Cannot close session ${this.sessionId}: client was not provided during session creation.`
385
+ );
386
+ return;
387
+ }
388
+ try {
389
+ getLogger().debug(`Closing session ${this.sessionId}`);
390
+ await this.client.sessions.delete({
391
+ sessionId: this.sessionId,
392
+ projectId: this.projectId
393
+ });
394
+ this.closed = true;
395
+ getLogger().info(`Session closed: ${this.sessionId}`);
396
+ } catch (error) {
397
+ getLogger().warn(`Failed to close session ${this.sessionId}:`, error);
246
398
  }
247
- const date = new Date(this.updatedAt);
248
- return Number.isNaN(date.getTime()) ? null : date;
249
- }
250
- isLocked() {
251
- return this.status === "locked";
252
- }
253
- isAvailable() {
254
- return this.status === "available";
255
399
  }
256
400
  };
257
- var ContextListResponse = class {
258
- constructor(data) {
259
- this.data = data;
401
+ var SessionListResponse = class {
402
+ constructor(sessions, pagination) {
403
+ this.sessions = sessions;
404
+ this.pagination = pagination;
260
405
  }
406
+ /**
407
+ * Number of sessions in the current page.
408
+ */
261
409
  get length() {
262
- return this.data.length;
410
+ return this.sessions.length;
263
411
  }
264
412
  [Symbol.iterator]() {
265
- return this.data[Symbol.iterator]();
413
+ return this.sessions[Symbol.iterator]();
266
414
  }
267
415
  at(index) {
268
- return this.data[index];
416
+ return this.sessions[index];
269
417
  }
270
418
  };
271
- var ContextsResource = class {
419
+ var SessionDownloadInfo = class {
420
+ constructor(shape) {
421
+ this.id = shape.id;
422
+ this.filename = shape.filename;
423
+ this.contentType = shape.contentType;
424
+ this.size = shape.size;
425
+ this.sha256 = shape.sha256;
426
+ this.status = shape.status;
427
+ this.createdAt = shape.createdAt;
428
+ }
429
+ };
430
+ var SessionDownloadsListResponse = class {
431
+ constructor(downloads, summary) {
432
+ this.downloads = downloads;
433
+ this.summary = summary;
434
+ }
435
+ };
436
+ var SessionDownloadsDeleteResponse = class {
437
+ constructor(shape) {
438
+ this.status = shape.status;
439
+ this.deletedCount = shape.deletedCount;
440
+ }
441
+ };
442
+ var SessionTargetInfo = class {
443
+ constructor(shape) {
444
+ this.id = shape.id;
445
+ this.title = shape.title;
446
+ this.type = shape.type;
447
+ this.url = shape.url;
448
+ this.description = shape.description;
449
+ this.inspectUrl = shape.inspectUrl;
450
+ this.webSocketDebuggerUrl = shape.webSocketDebuggerUrl;
451
+ this.webSocketDebuggerUrlTransformed = shape.webSocketDebuggerUrlTransformed;
452
+ }
453
+ };
454
+ var SessionDownloadsResource = class {
272
455
  constructor(client) {
273
456
  this.client = client;
274
457
  }
275
- /**
276
- * Create a new persistent context.
277
- */
278
- async create(options = {}) {
279
- const url = `${this.client.baseUrl}/instance/v1/contexts/create-context`;
280
- const payload = {
281
- api_key: this.client.apiKey,
282
- project_id: this.client.projectId
283
- };
284
- if (options.metadata) {
285
- payload.metadata = options.metadata;
286
- }
287
- const response = await this.client._post(url, payload);
458
+ async list(sessionId, projectId) {
459
+ const response = await this.client._post(
460
+ `${this.client.baseUrl}/instance/v1/sessions/${sessionId}/downloads/list`,
461
+ {
462
+ api_key: this.client.apiKey,
463
+ project_id: projectId ?? this.client.projectId
464
+ }
465
+ );
288
466
  if (response.status >= 400) {
289
- this.handleError(response, "create");
467
+ this.handleError("list session downloads", response, sessionId);
290
468
  }
291
469
  const result = asRecord(response.data);
292
- const contextId = getString(result.context_id);
293
- if (!contextId) {
294
- throw new APIError("Failed to create context: context_id missing from response", {
295
- statusCode: response.status,
296
- response: response.data
470
+ const items = Array.isArray(result.downloads) ? result.downloads : [];
471
+ const downloads = items.map((item) => {
472
+ const download = asRecord(item);
473
+ return new SessionDownloadInfo({
474
+ id: getString(download.id) ?? "",
475
+ filename: getString(download.filename) ?? "",
476
+ contentType: getString(download.content_type) ?? null,
477
+ size: getNumber(download.size) ?? 0,
478
+ sha256: getString(download.sha256) ?? null,
479
+ status: getString(download.status) ?? "available",
480
+ createdAt: getString(download.created_at) ?? ""
297
481
  });
298
- }
299
- const context = new ContextInfo({
300
- id: contextId,
301
- status: normalizeContextStatus(result.locked),
302
- metadata: result.metadata ?? options.metadata ?? {},
303
- createdAt: parseTimestamp(result.created_at)
304
482
  });
305
- getLogger().info(`Successfully created context '${context.id}'`);
306
- return context;
483
+ const summary = asRecord(result.summary);
484
+ return new SessionDownloadsListResponse(downloads, {
485
+ count: getNumber(summary.count) ?? downloads.length,
486
+ totalSize: getNumber(summary.total_size) ?? 0
487
+ });
307
488
  }
308
- /**
309
- * List contexts in the current project.
310
- */
311
- async list(options = {}) {
312
- const url = `${this.client.baseUrl}/instance/v1/contexts/list-contexts`;
313
- const payload = {
314
- api_key: this.client.apiKey,
315
- project_id: this.client.projectId,
316
- limit: options.limit ?? 20
317
- };
318
- if (options.status) {
319
- payload.status = options.status;
489
+ async get(sessionId, downloadId, projectId) {
490
+ const response = await this.client._get(
491
+ `${this.client.baseUrl}/instance/v1/sessions/${sessionId}/downloads/${downloadId}`,
492
+ {
493
+ api_key: this.client.apiKey,
494
+ project_id: projectId ?? this.client.projectId
495
+ },
496
+ {
497
+ responseType: "arraybuffer"
498
+ }
499
+ );
500
+ if (response.status >= 400) {
501
+ this.handleError("fetch session download", response, sessionId);
320
502
  }
321
- const response = await this.client._post(url, payload);
503
+ return Buffer.from(response.data);
504
+ }
505
+ async archive(sessionId, projectId) {
506
+ const response = await this.client._get(
507
+ `${this.client.baseUrl}/instance/v1/sessions/${sessionId}/downloads/archive`,
508
+ {
509
+ api_key: this.client.apiKey,
510
+ project_id: projectId ?? this.client.projectId
511
+ },
512
+ {
513
+ responseType: "arraybuffer"
514
+ }
515
+ );
322
516
  if (response.status >= 400) {
323
- this.handleError(response, "list");
517
+ this.handleError("archive session downloads", response, sessionId);
324
518
  }
325
- const result = asRecord(response.data);
326
- const contexts = Array.isArray(result.contexts) ? result.contexts.map((item) => {
327
- const context = asRecord(item);
328
- return new ContextInfo({
329
- id: getString(context.context_id) ?? "",
330
- status: normalizeContextStatus(context.locked),
331
- createdAt: parseTimestamp(context.created_at),
332
- updatedAt: parseTimestamp(context.updated_at),
333
- metadata: context.metadata ?? {}
334
- });
335
- }) : [];
336
- getLogger().info(`Retrieved ${contexts.length} contexts`);
337
- return new ContextListResponse(contexts);
519
+ return Buffer.from(response.data);
338
520
  }
339
- /**
340
- * Fetch a single context.
341
- */
342
- async get(contextId) {
343
- const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}`;
344
- const payload = {
345
- api_key: this.client.apiKey,
346
- project_id: this.client.projectId
347
- };
348
- const response = await this.client._post(url, payload);
521
+ async delete(sessionId, projectId) {
522
+ const response = await this.client._delete(
523
+ `${this.client.baseUrl}/instance/v1/sessions/${sessionId}/downloads`,
524
+ {
525
+ api_key: this.client.apiKey,
526
+ project_id: projectId ?? this.client.projectId
527
+ }
528
+ );
349
529
  if (response.status >= 400) {
350
- this.handleError(response, "get", contextId);
530
+ this.handleError("delete session downloads", response, sessionId);
351
531
  }
352
532
  const result = asRecord(response.data);
353
- const context = asRecord(result.context);
354
- const contextInfo = new ContextInfo({
355
- id: getString(context.context_id) ?? contextId,
356
- status: normalizeContextStatus(context.locked),
357
- metadata: context.metadata ?? {},
358
- createdAt: parseTimestamp(context.created_at),
359
- updatedAt: parseTimestamp(context.updated_at)
533
+ return new SessionDownloadsDeleteResponse({
534
+ status: getString(result.status) ?? "",
535
+ deletedCount: getNumber(result.deleted_count) ?? 0
360
536
  });
361
- getLogger().info(`Retrieved context '${contextInfo.id}' (status: ${contextInfo.status})`);
362
- return contextInfo;
537
+ }
538
+ handleError(action, response, sessionId) {
539
+ const errorData = asRecord(response.data);
540
+ const errorMessage = getString(errorData.error) ?? getString(errorData.message) ?? "Unknown error";
541
+ if (response.status === 401) {
542
+ throw new AuthenticationError(
543
+ `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
544
+ );
545
+ }
546
+ if (response.status === 404) {
547
+ throw new SessionNotFoundError(`Session downloads not found for ${sessionId}: ${errorMessage}`);
548
+ }
549
+ throw new APIError(`Failed to ${action}: ${errorMessage}`, {
550
+ statusCode: response.status,
551
+ response: response.data
552
+ });
553
+ }
554
+ };
555
+ var SessionsResource = class {
556
+ constructor(client) {
557
+ this.client = client;
558
+ this.downloads = new SessionDownloadsResource(client);
363
559
  }
364
560
  /**
365
- * Fork an existing persistent context into a new context.
561
+ * Create a new browser session.
366
562
  */
367
- async fork(contextId) {
368
- const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}/fork`;
563
+ async create(options = {}) {
369
564
  const payload = {
370
565
  api_key: this.client.apiKey,
371
- project_id: this.client.projectId
566
+ project_id: options.projectId ?? this.client.projectId,
567
+ browser_mode: options.browserMode ?? "normal"
372
568
  };
569
+ if (options.context) {
570
+ const contextPayload = {
571
+ id: options.context.id,
572
+ mode: normalizeContextMode(options.context.mode)
573
+ };
574
+ payload.context = contextPayload;
575
+ getLogger().debug(
576
+ `Creating session with context (id=${options.context.id}, mode=${contextPayload.mode})`
577
+ );
578
+ } else {
579
+ getLogger().debug(`Creating session with browser_mode=${payload.browser_mode}`);
580
+ }
581
+ if (options.extensionIds && options.extensionIds.length > 0) {
582
+ payload.extension_ids = options.extensionIds;
583
+ }
584
+ if (options.proxy) {
585
+ payload.proxy = this.normalizeProxy(options.proxy);
586
+ }
587
+ if (options.weakLock) {
588
+ payload.weak_lock = true;
589
+ }
590
+ if (options.asyncCreate !== false) {
591
+ return this.createAsync(payload, options);
592
+ }
593
+ const url = `${this.client.baseUrl}/instance`;
373
594
  const response = await this.client._post(url, payload);
374
595
  if (response.status >= 400) {
375
- this.handleError(response, "fork", contextId);
596
+ this.handleCreateError(response);
376
597
  }
377
598
  const result = asRecord(response.data);
378
- const forkedContextId = getString(result.context_id);
379
- if (!forkedContextId) {
380
- throw new APIError("Failed to fork context: context_id missing from response", {
599
+ const sessionId = getString(result.session_id);
600
+ if (!sessionId) {
601
+ throw new APIError("Failed to create session: session_id missing from response", {
381
602
  statusCode: response.status,
382
603
  response: response.data
383
604
  });
384
605
  }
385
- const contextInfo = new ContextInfo({
386
- id: forkedContextId,
387
- status: normalizeContextStatus(result.locked),
388
- metadata: result.metadata ?? {},
389
- createdAt: parseTimestamp(result.created_at),
390
- updatedAt: parseTimestamp(result.updated_at)
606
+ const containerId = getString(result.container_id) ?? null;
607
+ const projectId = options.projectId ?? this.client.projectId;
608
+ const createdSession = await this._getCreatedSession(sessionId, projectId);
609
+ const wsUrl = await this._getWebSocketDebuggerUrl(sessionId);
610
+ getLogger().info(`Session created successfully: id=${sessionId}, container_id=${containerId}`);
611
+ return new SessionInfo({
612
+ id: sessionId,
613
+ status: createdSession?.status ?? "active",
614
+ apiKey: this.client.apiKey,
615
+ projectId,
616
+ browserType: createdSession?.browserType ?? (options.browserMode ?? "normal"),
617
+ createdAt: createdSession?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
618
+ inspectUrl: createdSession?.inspectUrl ?? `${this.client.baseUrl}/inspect?session_id=${sessionId}`,
619
+ containerId: createdSession?.containerId ?? containerId,
620
+ ws: wsUrl ?? createdSession?.ws ?? null,
621
+ client: this.client
391
622
  });
392
- getLogger().info(`Successfully forked context '${contextId}' -> '${contextInfo.id}'`);
393
- return contextInfo;
623
+ }
624
+ async createAsync(payload, options) {
625
+ const response = await this.client._post(`${this.client.baseUrl}/instance/v2`, payload);
626
+ if (response.status >= 400) {
627
+ this.handleCreateError(response);
628
+ }
629
+ const result = asRecord(response.data);
630
+ const sessionId = getString(result.session_id);
631
+ if (!sessionId) {
632
+ throw new APIError("Failed to create session: session_id missing from response", {
633
+ statusCode: response.status,
634
+ response: response.data
635
+ });
636
+ }
637
+ const projectId = options.projectId ?? this.client.projectId;
638
+ const pollIntervalMs = options.pollIntervalMs ?? 1e3;
639
+ const pollTimeoutMs = options.pollTimeoutMs ?? 6e5;
640
+ const deadline = Date.now() + pollTimeoutMs;
641
+ while (Date.now() < deadline) {
642
+ const info = await this.get(sessionId, projectId);
643
+ if (info.status === "active") {
644
+ const wsUrl = await this._getWebSocketDebuggerUrl(sessionId);
645
+ getLogger().info(`Session created successfully (async): id=${sessionId}`);
646
+ return new SessionInfo({
647
+ id: sessionId,
648
+ status: "active",
649
+ apiKey: this.client.apiKey,
650
+ projectId,
651
+ browserType: info.browserType || (options.browserMode ?? "normal"),
652
+ createdAt: info.createdAt,
653
+ inspectUrl: info.inspectUrl,
654
+ containerId: info.containerId,
655
+ ws: wsUrl ?? info.ws,
656
+ client: this.client
657
+ });
658
+ }
659
+ if (info.status === "create_failed") {
660
+ this.handleAsyncCreateFailed(payload);
661
+ throw new APIError("Session creation failed", {
662
+ statusCode: 500,
663
+ response: { session_id: sessionId, status: info.status }
664
+ });
665
+ }
666
+ if (info.status === "closed") {
667
+ throw new APIError("Session closed before activation", {
668
+ statusCode: 500,
669
+ response: { session_id: sessionId, status: info.status }
670
+ });
671
+ }
672
+ await delay(pollIntervalMs);
673
+ }
674
+ throw new TimeoutError(
675
+ `Timed out waiting for session ${sessionId} to become active after ${pollTimeoutMs}ms`
676
+ );
394
677
  }
395
678
  /**
396
- * Delete a persistent context.
679
+ * Fetch one session by id.
680
+ *
681
+ * The upstream API uses POST /instance/session for this lookup, so this SDK method
682
+ * intentionally uses POST even though the operation is read-only.
397
683
  */
398
- async delete(contextId) {
399
- const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}`;
400
- const payload = {
684
+ async get(sessionId, projectId) {
685
+ const response = await this.client._post(`${this.client.baseUrl}/instance/session`, {
401
686
  api_key: this.client.apiKey,
402
- project_id: this.client.projectId
403
- };
404
- const response = await this.client._delete(url, payload);
687
+ project_id: projectId ?? this.client.projectId,
688
+ session_id: sessionId
689
+ });
405
690
  if (response.status >= 400) {
406
- this.handleError(response, "delete", contextId);
691
+ this.handleGetError(response, sessionId);
407
692
  }
408
- getLogger().info(`Successfully deleted context '${contextId}'`);
693
+ return this.mapSessionInfo(asRecord(response.data), projectId);
409
694
  }
410
695
  /**
411
- * Force-release a stuck lock on a context.
696
+ * List sessions for the current project.
412
697
  */
413
- async forceRelease(contextId) {
414
- const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}/force-release`;
698
+ async list(options = {}) {
699
+ const url = `${this.client.baseUrl}/instance/v2/sessions`;
415
700
  const payload = {
416
701
  api_key: this.client.apiKey,
417
- project_id: this.client.projectId
702
+ project_id: options.projectId ?? this.client.projectId
418
703
  };
704
+ if (options.status) {
705
+ payload.status = options.status;
706
+ }
419
707
  const response = await this.client._post(url, payload);
420
708
  if (response.status >= 400) {
421
- this.handleError(response, "force_release", contextId);
709
+ this.handleListError(response);
422
710
  }
423
711
  const result = asRecord(response.data);
424
- return {
425
- status: getString(result.status) ?? "unlocked",
426
- message: getString(result.message) ?? "Lock released successfully"
427
- };
712
+ const sessionsData = Array.isArray(result.sessions) ? result.sessions : [];
713
+ const paginationData = asRecord(result.pagination);
714
+ const sessions = sessionsData.map((item) => this.mapSessionInfo(asRecord(item), options.projectId));
715
+ const pagination = new PaginationInfo({
716
+ currentPage: getNumber(paginationData.currentPage) ?? 1,
717
+ pageSize: getNumber(paginationData.pageSize) ?? sessions.length,
718
+ totalCount: getNumber(paginationData.totalCount) ?? sessions.length,
719
+ totalPages: getNumber(paginationData.totalPages) ?? 1,
720
+ activeCount: getNumber(paginationData.activeCount) ?? 0,
721
+ closedCount: getNumber(paginationData.closedCount) ?? 0
722
+ });
723
+ getLogger().info(
724
+ `Retrieved ${sessions.length} sessions (total: ${pagination.totalCount}, active: ${pagination.activeCount}, closed: ${pagination.closedCount})`
725
+ );
726
+ return new SessionListResponse(sessions, pagination);
428
727
  }
429
- handleError(response, operation, contextId) {
430
- const errorData = asRecord(response.data);
728
+ /**
729
+ * Delete a browser session.
730
+ */
731
+ async delete(options) {
732
+ const url = `${this.client.baseUrl}/instance`;
733
+ const payload = {
734
+ api_key: this.client.apiKey,
735
+ project_id: options.projectId ?? this.client.projectId,
736
+ session_id: options.sessionId
737
+ };
738
+ const response = await this.client._delete(url, payload);
739
+ if (response.status >= 400) {
740
+ this.handleDeleteError(response, options.sessionId);
741
+ }
742
+ getLogger().info(`Session deleted successfully: ${options.sessionId}`);
743
+ }
744
+ /**
745
+ * List target/page metadata for a session via `/json`.
746
+ */
747
+ async listTargets(sessionId) {
748
+ const response = await this.client._get(`${this.client.baseUrl}/json`, {
749
+ session_id: sessionId
750
+ });
751
+ if (response.status >= 400) {
752
+ this.handleListTargetsError(response, sessionId);
753
+ }
754
+ const result = Array.isArray(response.data) ? response.data : [];
755
+ return result.map((item) => this.mapSessionTarget(asRecord(item)));
756
+ }
757
+ /**
758
+ * Fetch the debugger WebSocket URL for a session.
759
+ *
760
+ * @internal
761
+ */
762
+ async _getWebSocketDebuggerUrl(sessionId) {
763
+ try {
764
+ const response = await this.client._get(`${this.client.baseUrl}/json/version`, {
765
+ session_id: sessionId
766
+ });
767
+ if (response.status >= 400) {
768
+ getLogger().error("Error getting WebSocket debugger URL:", response.data);
769
+ return null;
770
+ }
771
+ const result = asRecord(response.data);
772
+ return getString(result.webSocketDebuggerUrlTransformed) ?? getString(result.webSocketDebuggerUrl) ?? null;
773
+ } catch (error) {
774
+ getLogger().error("Error getting WebSocket debugger URL:", error);
775
+ return null;
776
+ }
777
+ }
778
+ normalizeProxy(proxy) {
779
+ if (!proxy.server) {
780
+ throw new ValidationError("proxy.server is required when proxy is provided");
781
+ }
782
+ const normalizedProxy = {
783
+ type: proxy.type ?? "external",
784
+ server: proxy.server
785
+ };
786
+ if (proxy.username) {
787
+ normalizedProxy.username = proxy.username;
788
+ }
789
+ if (proxy.password) {
790
+ normalizedProxy.password = proxy.password;
791
+ }
792
+ return normalizedProxy;
793
+ }
794
+ handleAsyncCreateFailed(payload) {
795
+ const context = asRecord(payload.context);
796
+ const contextId = getString(context.id);
797
+ const contextMode = getString(context.mode);
798
+ if (contextId && contextMode === "read_write" && payload.weak_lock !== true) {
799
+ throw new ContextLockedError("Context is locked by another active session");
800
+ }
801
+ }
802
+ handleCreateError(response) {
803
+ const errorData = asRecord(response.data);
431
804
  const errorMessage = getString(errorData.error) ?? getString(errorData.message) ?? "Unknown error";
432
805
  const errorCode = getString(errorData.code);
433
806
  const metadata = asRecord(errorData.metadata);
434
807
  if (response.status === 401) {
435
- throw new AuthenticationError(`Authentication failed: ${errorMessage}`);
808
+ throw new AuthenticationError(
809
+ `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
810
+ );
811
+ }
812
+ if (response.status === 404 && errorCode === "context_not_found") {
813
+ throw new ContextNotFoundError(`Context not found: ${errorMessage}`);
436
814
  }
437
815
  if (response.status === 404) {
438
- const suffix = contextId ? `: ${contextId}` : "";
439
- throw new ContextNotFoundError(`Context not found${suffix}`);
816
+ throw new SessionNotFoundError(`Resource not found: ${errorMessage}`);
440
817
  }
441
- if (response.status === 409 && (errorCode === "context_locked" || errorCode === "CONTEXT_FORK_SOURCE_LOCKED")) {
818
+ if (response.status === 409 && errorCode === "context_locked") {
442
819
  throw new ContextLockedError(errorMessage, {
443
820
  activeSessionId: getString(metadata.activeSessionId),
444
- retryAfter: typeof metadata.retryAfter === "number" ? metadata.retryAfter : void 0
821
+ retryAfter: getNumber(metadata.retryAfter)
445
822
  });
446
823
  }
447
- if (response.status === 409 && errorCode === "session_active") {
448
- throw new APIError(
449
- `Cannot ${operation} context: ${errorMessage} (session is active)`,
450
- {
451
- statusCode: response.status,
452
- response: response.data
453
- }
824
+ throw new APIError(`Failed to create session: ${errorMessage}`, {
825
+ statusCode: response.status,
826
+ response: response.data
827
+ });
828
+ }
829
+ handleGetError(response, sessionId) {
830
+ const errorData = asRecord(response.data);
831
+ const errorMessage = getString(errorData.error) ?? getString(errorData.message) ?? "Unknown error";
832
+ if (response.status === 401) {
833
+ throw new AuthenticationError(
834
+ `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
454
835
  );
455
836
  }
456
- if (response.status === 500) {
457
- const details = getString(errorData.details);
458
- throw new APIError(
459
- `Internal server error (500) during ${operation}: ${errorMessage}${details ? `. Details: ${details}` : ""}`,
460
- {
461
- statusCode: response.status,
462
- response: response.data
463
- }
837
+ if (response.status === 404) {
838
+ throw new SessionNotFoundError(
839
+ `Session not found: ${errorMessage}. Session ID '${sessionId}' may not exist in this project.`
464
840
  );
465
841
  }
466
- if (response.status === 502) {
467
- throw new APIError(
468
- `Server gateway error (502) during ${operation}: The service may be temporarily unavailable. Please retry.`,
469
- {
470
- statusCode: response.status,
471
- response: response.data
472
- }
842
+ throw new APIError(`Failed to get session: ${errorMessage}`, {
843
+ statusCode: response.status,
844
+ response: response.data
845
+ });
846
+ }
847
+ handleListError(response) {
848
+ const errorData = asRecord(response.data);
849
+ const errorMessage = getString(errorData.error) ?? getString(errorData.message) ?? "Unknown error";
850
+ if (response.status === 401) {
851
+ throw new AuthenticationError(
852
+ `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
473
853
  );
474
854
  }
475
- if (response.status === 503) {
476
- throw new APIError(
477
- `Service unavailable (503) during ${operation}: The service is temporarily unavailable. Please retry later.`,
478
- {
479
- statusCode: response.status,
480
- response: response.data
481
- }
855
+ throw new APIError(`Failed to list sessions: ${errorMessage}`, {
856
+ statusCode: response.status,
857
+ response: response.data
858
+ });
859
+ }
860
+ handleListTargetsError(response, sessionId) {
861
+ const errorData = asRecord(response.data);
862
+ const errorMessage = getString(errorData.error) ?? getString(errorData.message) ?? "Unknown error";
863
+ if (response.status === 401) {
864
+ throw new AuthenticationError(
865
+ `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
482
866
  );
483
867
  }
484
- if (response.status === 504) {
485
- throw new APIError(
486
- `Gateway timeout (504) during ${operation}: The request timed out. Please retry.`,
487
- {
488
- statusCode: response.status,
489
- response: response.data
490
- }
868
+ if (response.status === 404) {
869
+ throw new SessionNotFoundError(
870
+ `Session not found: ${errorMessage}. Session ID '${sessionId}' may not exist in this project.`
491
871
  );
492
872
  }
493
- throw new APIError(`Failed to ${operation} context: ${errorMessage}`, {
873
+ throw new APIError(`Failed to list session targets: ${errorMessage}`, {
494
874
  statusCode: response.status,
495
875
  response: response.data
496
876
  });
497
877
  }
878
+ handleDeleteError(response, sessionId) {
879
+ const errorData = asRecord(response.data);
880
+ const errorMessage = getString(errorData.error) ?? getString(errorData.message) ?? "Unknown error";
881
+ if (response.status === 401) {
882
+ throw new AuthenticationError(
883
+ `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
884
+ );
885
+ }
886
+ if (response.status === 404) {
887
+ throw new SessionNotFoundError(
888
+ `Session not found: ${errorMessage}. Session ID '${sessionId}' may have already been deleted or never existed.`
889
+ );
890
+ }
891
+ throw new APIError(`Failed to delete session: ${errorMessage}`, {
892
+ statusCode: response.status,
893
+ response: response.data
894
+ });
895
+ }
896
+ mapSessionInfo(raw, projectId) {
897
+ const sessionId = getString(raw.session_id) ?? getString(raw.id) ?? "";
898
+ return new SessionInfo({
899
+ id: sessionId,
900
+ status: getString(raw.status) ?? "active",
901
+ apiKey: getString(raw.api_key) ?? this.client.apiKey,
902
+ projectId: getString(raw.project_id) ?? (projectId ?? this.client.projectId),
903
+ regionId: getString(raw.region_id) ?? this.client.selectedRegion ?? this.client.region ?? null,
904
+ browserType: getString(raw.browser_type) ?? getString(raw.browser_mode) ?? "normal",
905
+ createdAt: getString(raw.created_at) ?? "",
906
+ inspectUrl: getString(raw.inspect_url) ?? `${this.client.baseUrl}/inspect?session_id=${sessionId}`,
907
+ containerId: getString(raw.container_id) ?? null,
908
+ ws: getString(raw.ws) ?? null,
909
+ client: this.client
910
+ });
911
+ }
912
+ mapSessionTarget(raw) {
913
+ return new SessionTargetInfo({
914
+ id: getString(raw.id) ?? "",
915
+ title: getString(raw.title) ?? "",
916
+ type: getString(raw.type) ?? "",
917
+ url: getString(raw.url) ?? "",
918
+ description: getString(raw.description) ?? "",
919
+ inspectUrl: getString(raw.inspectUrl) ?? getString(raw.inspect_url) ?? null,
920
+ webSocketDebuggerUrl: getString(raw.webSocketDebuggerUrl) ?? getString(raw.web_socket_debugger_url) ?? null,
921
+ webSocketDebuggerUrlTransformed: getString(raw.webSocketDebuggerUrlTransformed) ?? getString(raw.web_socket_debugger_url_transformed) ?? null
922
+ });
923
+ }
924
+ async _getCreatedSession(sessionId, projectId) {
925
+ try {
926
+ return await this.get(sessionId, projectId);
927
+ } catch (error) {
928
+ getLogger().warn(
929
+ `Failed to fetch created session ${sessionId}; falling back to locally derived session fields:`,
930
+ error
931
+ );
932
+ return null;
933
+ }
934
+ }
498
935
  };
499
936
 
500
- // src/extensions.ts
501
- var import_promises = require("fs/promises");
502
- var import_node_path = __toESM(require("path"));
937
+ // src/auth.ts
938
+ var integratedAuthCallbacks = /* @__PURE__ */ new Map();
939
+ var cdpClients = /* @__PURE__ */ new Map();
940
+ function getIntegratedAuthKey(sessionOrClient) {
941
+ const session = sessionOrClient instanceof SessionInfo ? sessionOrClient : sessionOrClient.session;
942
+ const connectUrl = session.connectUrl;
943
+ if (!connectUrl) {
944
+ throw new ValidationError(`Session ${session.sessionId} does not include a CDP connect URL`);
945
+ }
946
+ return connectUrl;
947
+ }
948
+ async function registerIntegratedAuthCallback(sessionOrClient, callback) {
949
+ const key = getIntegratedAuthKey(sessionOrClient);
950
+ let sessionCallbacks = integratedAuthCallbacks.get(key);
951
+ if (sessionCallbacks?.has(callback)) {
952
+ return;
953
+ }
954
+ let client = cdpClients.get(key);
955
+ if (!client) {
956
+ if (sessionOrClient instanceof SessionInfo) {
957
+ client = await connectOverCDP(sessionOrClient);
958
+ } else {
959
+ client = sessionOrClient;
960
+ }
961
+ await client.send("Fetch.enable", { handleAuthRequests: true });
962
+ cdpClients.set(key, client);
963
+ client.on("disconnect", () => {
964
+ cdpClients.delete(key);
965
+ integratedAuthCallbacks.delete(key);
966
+ });
967
+ client.on("Fetch.requestPaused", async (event, sessionId) => {
968
+ try {
969
+ await client.send(
970
+ "Fetch.continueRequest",
971
+ {
972
+ requestId: event.requestId
973
+ },
974
+ sessionId
975
+ );
976
+ } catch (error) {
977
+ getLogger().error("Failed to continue paused fetch request", error);
978
+ }
979
+ });
980
+ client.on("Fetch.integratedAuthRequired", async (params, sessionId) => {
981
+ try {
982
+ const sessionCallbacks2 = integratedAuthCallbacks.get(key);
983
+ if (!sessionCallbacks2) {
984
+ await client.send(
985
+ "Fetch.continueWithIntegratedAuth",
986
+ {
987
+ requestId: params.requestId,
988
+ authChallengeResponse: {
989
+ response: "CancelAuth"
990
+ }
991
+ },
992
+ sessionId
993
+ );
994
+ return;
995
+ }
996
+ const sender = async (result) => {
997
+ if (result.cancel) {
998
+ await client.send(
999
+ "Fetch.continueWithIntegratedAuth",
1000
+ {
1001
+ requestId: params.requestId,
1002
+ authChallengeResponse: {
1003
+ response: "CancelAuth"
1004
+ }
1005
+ },
1006
+ sessionId
1007
+ );
1008
+ return;
1009
+ }
1010
+ await client.send(
1011
+ "Fetch.continueWithIntegratedAuth",
1012
+ {
1013
+ requestId: params.requestId,
1014
+ authChallengeResponse: {
1015
+ response: "ProvideIntegratedCredentials",
1016
+ scheme: result.scheme,
1017
+ token: result.token
1018
+ }
1019
+ },
1020
+ sessionId
1021
+ );
1022
+ };
1023
+ for (const cb of sessionCallbacks2) {
1024
+ try {
1025
+ const result = cb(params, sender);
1026
+ void Promise.resolve(result).catch((error) => {
1027
+ getLogger().error("Integrated auth callback failed", error);
1028
+ });
1029
+ } catch (error) {
1030
+ getLogger().error("Integrated auth callback failed", error);
1031
+ }
1032
+ }
1033
+ } catch (error) {
1034
+ getLogger().error("Failed to handle integrated auth request", error);
1035
+ }
1036
+ });
1037
+ }
1038
+ if (!sessionCallbacks) {
1039
+ sessionCallbacks = /* @__PURE__ */ new Set();
1040
+ integratedAuthCallbacks.set(key, sessionCallbacks);
1041
+ }
1042
+ sessionCallbacks.add(callback);
1043
+ }
1044
+ function deregisterIntegratedAuthCallback(sessionOrClient, callback) {
1045
+ const key = getIntegratedAuthKey(sessionOrClient);
1046
+ const sessionCallbacks = integratedAuthCallbacks.get(key);
1047
+ if (!sessionCallbacks?.has(callback)) {
1048
+ return;
1049
+ }
1050
+ sessionCallbacks.delete(callback);
1051
+ if (sessionCallbacks.size === 0) {
1052
+ integratedAuthCallbacks.delete(key);
1053
+ }
1054
+ }
1055
+
1056
+ // src/client.ts
1057
+ var import_axios = require("axios");
1058
+ var import_axios2 = __toESM(require("axios"));
1059
+ var dotenv = __toESM(require("dotenv"));
1060
+
1061
+ // src/contexts.ts
503
1062
  function asRecord2(value) {
504
1063
  return typeof value === "object" && value !== null ? value : {};
505
1064
  }
506
1065
  function getString2(value) {
507
1066
  return typeof value === "string" ? value : void 0;
508
1067
  }
509
- var ExtensionInfo = class {
510
- constructor(shape) {
511
- this.id = shape.id;
512
- this.name = shape.name;
513
- this.projectId = shape.projectId;
514
- this.createdAt = shape.createdAt;
515
- this.updatedAt = shape.updatedAt;
1068
+ function parseTimestamp(value) {
1069
+ if (typeof value === "string" && value.length > 0) {
1070
+ return value;
516
1071
  }
517
- };
518
- var ExtensionsResource = class {
519
- constructor(client) {
520
- this.client = client;
1072
+ if (typeof value === "number" && Number.isFinite(value)) {
1073
+ return new Date(value).toISOString();
521
1074
  }
522
- async upload(filePath, options = {}) {
523
- const fileBuffer = await (0, import_promises.readFile)(filePath);
524
- const form = new FormData();
525
- const usedProjectId = options.projectId ?? this.client.projectId;
526
- form.append("project_id", usedProjectId);
527
- if (options.name) {
528
- form.append("name", options.name);
529
- }
530
- form.append(
531
- "file",
532
- new Blob([fileBuffer], { type: "application/octet-stream" }),
533
- import_node_path.default.basename(filePath)
534
- );
535
- const response = await this.client._post(
536
- `${this.client.baseUrl}/instance/v1/extension/upload`,
537
- form,
538
- {
539
- headers: {
540
- project_id: usedProjectId
541
- }
542
- }
543
- );
544
- if (response.status >= 400) {
545
- this.handleError("upload extension", response);
546
- }
547
- getLogger().info(`Extension uploaded successfully: ${filePath}`);
548
- return this.parseInfo(response.data);
549
- }
550
- async list(options = {}) {
551
- const response = await this.client._post(`${this.client.baseUrl}/instance/v1/extension/list`, {
552
- project_id: options.projectId ?? this.client.projectId,
553
- limit: options.limit ?? 20,
554
- offset: options.offset ?? 0
555
- });
556
- if (response.status >= 400) {
557
- this.handleError("list extensions", response);
558
- }
559
- const result = asRecord2(response.data);
560
- const items = Array.isArray(result.data) ? result.data : [];
561
- return items.map((item) => this.parseInfo(item));
562
- }
563
- async get(extensionId, projectId) {
564
- const response = await this.client._post(`${this.client.baseUrl}/instance/v1/extension/info`, {
565
- project_id: projectId ?? this.client.projectId,
566
- extension_id: extensionId
567
- });
568
- if (response.status >= 400) {
569
- this.handleError("get extension", response);
570
- }
571
- return this.parseInfo(response.data);
572
- }
573
- async delete(extensionId) {
574
- const response = await this.client._delete(
575
- `${this.client.baseUrl}/instance/v1/extension/${extensionId}`
576
- );
577
- if (response.status >= 400) {
578
- this.handleError("delete extension", response);
579
- }
580
- getLogger().info(`Extension deleted successfully: ${extensionId}`);
581
- }
582
- parseInfo(data) {
583
- const item = asRecord2(data);
584
- return new ExtensionInfo({
585
- id: getString2(item.id) ?? "",
586
- name: getString2(item.name) ?? "",
587
- projectId: getString2(item.project_id) ?? "",
588
- createdAt: getString2(item.created_at),
589
- updatedAt: getString2(item.updated_at)
590
- });
591
- }
592
- handleError(action, response) {
593
- const errorData = asRecord2(response.data);
594
- const errorMessage = getString2(errorData.message) ?? getString2(errorData.error) ?? "Unknown error";
595
- if (response.status === 401) {
596
- throw new AuthenticationError(
597
- `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
598
- );
599
- }
600
- throw new APIError(`Failed to ${action}: ${errorMessage}`, {
601
- statusCode: response.status,
602
- response: response.data
603
- });
604
- }
605
- };
606
-
607
- // src/sessions.ts
608
- function asRecord3(value) {
609
- return typeof value === "object" && value !== null ? value : {};
610
- }
611
- function getString3(value) {
612
- return typeof value === "string" ? value : void 0;
613
- }
614
- function getNumber(value) {
615
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1075
+ return null;
616
1076
  }
617
- function normalizeContextMode(mode) {
618
- if (mode === "readWrite" || mode === "read_write") {
619
- return "read_write";
1077
+ function normalizeContextStatus(value) {
1078
+ if (value === true || value === "locked") {
1079
+ return "locked";
620
1080
  }
621
- return "read_only";
622
- }
623
- function delay(ms) {
624
- return new Promise((resolve) => {
625
- setTimeout(resolve, ms);
626
- });
627
- }
628
- var PaginationInfo = class {
629
- constructor(shape) {
630
- this.currentPage = shape.currentPage;
631
- this.pageSize = shape.pageSize;
632
- this.totalCount = shape.totalCount;
633
- this.totalPages = shape.totalPages;
634
- this.activeCount = shape.activeCount;
635
- this.closedCount = shape.closedCount;
1081
+ if (value === false || value === "available") {
1082
+ return "available";
636
1083
  }
637
- };
638
- var SessionInfo = class {
639
- /** @internal */
1084
+ return typeof value === "string" ? value : "available";
1085
+ }
1086
+ var ContextInfo = class {
640
1087
  constructor(options) {
641
- this.closed = false;
642
1088
  this.id = options.id;
643
- this.sessionId = options.id;
644
1089
  this.status = options.status;
645
- this.apiKey = options.apiKey;
646
- this.projectId = options.projectId;
647
- this.regionId = options.regionId ?? null;
648
- this.region_id = this.regionId;
649
- this.browserType = options.browserType;
650
- this.createdAt = options.createdAt;
651
- this.inspectUrl = options.inspectUrl;
652
- this.containerId = options.containerId;
653
- this.ws = options.ws ?? null;
654
- this.client = options.client;
655
- }
656
- /**
657
- * Playwright/CDP connection URL.
658
- */
659
- get connectUrl() {
660
- return this.ws ?? "";
1090
+ this.metadata = options.metadata ?? {};
1091
+ this.createdAt = options.createdAt ?? null;
1092
+ this.updatedAt = options.updatedAt ?? null;
661
1093
  }
662
- /**
663
- * Parsed session creation time.
664
- */
665
1094
  get createdAtDate() {
1095
+ if (!this.createdAt) {
1096
+ return null;
1097
+ }
666
1098
  const date = new Date(this.createdAt);
667
1099
  return Number.isNaN(date.getTime()) ? null : date;
668
1100
  }
669
- /**
670
- * Close the current session.
671
- *
672
- * This method is idempotent. Errors are logged and swallowed to keep cleanup safe.
673
- */
674
- async close() {
675
- if (this.closed) {
676
- getLogger().debug(`Session ${this.sessionId} is already closed.`);
677
- return;
678
- }
679
- if (!this.client) {
680
- getLogger().warn(
681
- `Cannot close session ${this.sessionId}: client was not provided during session creation.`
682
- );
683
- return;
684
- }
685
- try {
686
- getLogger().debug(`Closing session ${this.sessionId}`);
687
- await this.client.sessions.delete({
688
- sessionId: this.sessionId,
689
- projectId: this.projectId
690
- });
691
- this.closed = true;
692
- getLogger().info(`Session closed: ${this.sessionId}`);
693
- } catch (error) {
694
- getLogger().warn(`Failed to close session ${this.sessionId}:`, error);
695
- }
696
- }
697
- };
698
- var SessionListResponse = class {
699
- constructor(sessions, pagination) {
700
- this.sessions = sessions;
701
- this.pagination = pagination;
702
- }
703
- /**
704
- * Number of sessions in the current page.
705
- */
706
- get length() {
707
- return this.sessions.length;
708
- }
709
- [Symbol.iterator]() {
710
- return this.sessions[Symbol.iterator]();
711
- }
712
- at(index) {
713
- return this.sessions[index];
714
- }
715
- };
716
- var SessionDownloadInfo = class {
717
- constructor(shape) {
718
- this.id = shape.id;
719
- this.filename = shape.filename;
720
- this.contentType = shape.contentType;
721
- this.size = shape.size;
722
- this.sha256 = shape.sha256;
723
- this.status = shape.status;
724
- this.createdAt = shape.createdAt;
725
- }
726
- };
727
- var SessionDownloadsListResponse = class {
728
- constructor(downloads, summary) {
729
- this.downloads = downloads;
730
- this.summary = summary;
731
- }
732
- };
733
- var SessionDownloadsDeleteResponse = class {
734
- constructor(shape) {
735
- this.status = shape.status;
736
- this.deletedCount = shape.deletedCount;
737
- }
738
- };
739
- var SessionTargetInfo = class {
740
- constructor(shape) {
741
- this.id = shape.id;
742
- this.title = shape.title;
743
- this.type = shape.type;
744
- this.url = shape.url;
745
- this.description = shape.description;
746
- this.inspectUrl = shape.inspectUrl;
747
- this.webSocketDebuggerUrl = shape.webSocketDebuggerUrl;
748
- this.webSocketDebuggerUrlTransformed = shape.webSocketDebuggerUrlTransformed;
749
- }
750
- };
751
- var SessionDownloadsResource = class {
752
- constructor(client) {
753
- this.client = client;
754
- }
755
- async list(sessionId, projectId) {
756
- const response = await this.client._post(
757
- `${this.client.baseUrl}/instance/v1/sessions/${sessionId}/downloads/list`,
758
- {
759
- api_key: this.client.apiKey,
760
- project_id: projectId ?? this.client.projectId
761
- }
762
- );
763
- if (response.status >= 400) {
764
- this.handleError("list session downloads", response, sessionId);
765
- }
766
- const result = asRecord3(response.data);
767
- const items = Array.isArray(result.downloads) ? result.downloads : [];
768
- const downloads = items.map((item) => {
769
- const download = asRecord3(item);
770
- return new SessionDownloadInfo({
771
- id: getString3(download.id) ?? "",
772
- filename: getString3(download.filename) ?? "",
773
- contentType: getString3(download.content_type) ?? null,
774
- size: getNumber(download.size) ?? 0,
775
- sha256: getString3(download.sha256) ?? null,
776
- status: getString3(download.status) ?? "available",
777
- createdAt: getString3(download.created_at) ?? ""
778
- });
779
- });
780
- const summary = asRecord3(result.summary);
781
- return new SessionDownloadsListResponse(downloads, {
782
- count: getNumber(summary.count) ?? downloads.length,
783
- totalSize: getNumber(summary.total_size) ?? 0
784
- });
785
- }
786
- async get(sessionId, downloadId, projectId) {
787
- const response = await this.client._get(
788
- `${this.client.baseUrl}/instance/v1/sessions/${sessionId}/downloads/${downloadId}`,
789
- {
790
- api_key: this.client.apiKey,
791
- project_id: projectId ?? this.client.projectId
792
- },
793
- {
794
- responseType: "arraybuffer"
795
- }
796
- );
797
- if (response.status >= 400) {
798
- this.handleError("fetch session download", response, sessionId);
799
- }
800
- return Buffer.from(response.data);
801
- }
802
- async archive(sessionId, projectId) {
803
- const response = await this.client._get(
804
- `${this.client.baseUrl}/instance/v1/sessions/${sessionId}/downloads/archive`,
805
- {
806
- api_key: this.client.apiKey,
807
- project_id: projectId ?? this.client.projectId
808
- },
809
- {
810
- responseType: "arraybuffer"
811
- }
812
- );
813
- if (response.status >= 400) {
814
- this.handleError("archive session downloads", response, sessionId);
815
- }
816
- return Buffer.from(response.data);
817
- }
818
- async delete(sessionId, projectId) {
819
- const response = await this.client._delete(
820
- `${this.client.baseUrl}/instance/v1/sessions/${sessionId}/downloads`,
821
- {
822
- api_key: this.client.apiKey,
823
- project_id: projectId ?? this.client.projectId
824
- }
825
- );
826
- if (response.status >= 400) {
827
- this.handleError("delete session downloads", response, sessionId);
828
- }
829
- const result = asRecord3(response.data);
830
- return new SessionDownloadsDeleteResponse({
831
- status: getString3(result.status) ?? "",
832
- deletedCount: getNumber(result.deleted_count) ?? 0
833
- });
834
- }
835
- handleError(action, response, sessionId) {
836
- const errorData = asRecord3(response.data);
837
- const errorMessage = getString3(errorData.error) ?? getString3(errorData.message) ?? "Unknown error";
838
- if (response.status === 401) {
839
- throw new AuthenticationError(
840
- `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
841
- );
842
- }
843
- if (response.status === 404) {
844
- throw new SessionNotFoundError(`Session downloads not found for ${sessionId}: ${errorMessage}`);
1101
+ get updatedAtDate() {
1102
+ if (!this.updatedAt) {
1103
+ return null;
845
1104
  }
846
- throw new APIError(`Failed to ${action}: ${errorMessage}`, {
847
- statusCode: response.status,
848
- response: response.data
849
- });
1105
+ const date = new Date(this.updatedAt);
1106
+ return Number.isNaN(date.getTime()) ? null : date;
1107
+ }
1108
+ isLocked() {
1109
+ return this.status === "locked";
1110
+ }
1111
+ isAvailable() {
1112
+ return this.status === "available";
850
1113
  }
851
1114
  };
852
- var SessionsResource = class {
1115
+ var ContextListResponse = class {
1116
+ constructor(data) {
1117
+ this.data = data;
1118
+ }
1119
+ get length() {
1120
+ return this.data.length;
1121
+ }
1122
+ [Symbol.iterator]() {
1123
+ return this.data[Symbol.iterator]();
1124
+ }
1125
+ at(index) {
1126
+ return this.data[index];
1127
+ }
1128
+ };
1129
+ var ContextsResource = class {
853
1130
  constructor(client) {
854
1131
  this.client = client;
855
- this.downloads = new SessionDownloadsResource(client);
856
1132
  }
857
1133
  /**
858
- * Create a new browser session.
1134
+ * Create a new persistent context.
859
1135
  */
860
1136
  async create(options = {}) {
1137
+ const url = `${this.client.baseUrl}/instance/v1/contexts/create-context`;
861
1138
  const payload = {
862
1139
  api_key: this.client.apiKey,
863
- project_id: options.projectId ?? this.client.projectId,
864
- browser_mode: options.browserMode ?? "normal"
1140
+ project_id: this.client.projectId
865
1141
  };
866
- if (options.context) {
867
- const contextPayload = {
868
- id: options.context.id,
869
- mode: normalizeContextMode(options.context.mode)
870
- };
871
- payload.context = contextPayload;
872
- getLogger().debug(
873
- `Creating session with context (id=${options.context.id}, mode=${contextPayload.mode})`
874
- );
875
- } else {
876
- getLogger().debug(`Creating session with browser_mode=${payload.browser_mode}`);
877
- }
878
- if (options.extensionIds && options.extensionIds.length > 0) {
879
- payload.extension_ids = options.extensionIds;
880
- }
881
- if (options.proxy) {
882
- payload.proxy = this.normalizeProxy(options.proxy);
883
- }
884
- if (options.weakLock) {
885
- payload.weak_lock = true;
886
- }
887
- if (options.asyncCreate !== false) {
888
- return this.createAsync(payload, options);
1142
+ if (options.metadata) {
1143
+ payload.metadata = options.metadata;
889
1144
  }
890
- const url = `${this.client.baseUrl}/instance`;
891
1145
  const response = await this.client._post(url, payload);
892
1146
  if (response.status >= 400) {
893
- this.handleCreateError(response);
1147
+ this.handleError(response, "create");
894
1148
  }
895
- const result = asRecord3(response.data);
896
- const sessionId = getString3(result.session_id);
897
- if (!sessionId) {
898
- throw new APIError("Failed to create session: session_id missing from response", {
1149
+ const result = asRecord2(response.data);
1150
+ const contextId = getString2(result.context_id);
1151
+ if (!contextId) {
1152
+ throw new APIError("Failed to create context: context_id missing from response", {
899
1153
  statusCode: response.status,
900
1154
  response: response.data
901
1155
  });
902
1156
  }
903
- const containerId = getString3(result.container_id) ?? null;
904
- const projectId = options.projectId ?? this.client.projectId;
905
- const createdSession = await this._getCreatedSession(sessionId, projectId);
906
- const wsUrl = await this._getWebSocketDebuggerUrl(sessionId);
907
- getLogger().info(`Session created successfully: id=${sessionId}, container_id=${containerId}`);
908
- return new SessionInfo({
909
- id: sessionId,
910
- status: createdSession?.status ?? "active",
911
- apiKey: this.client.apiKey,
912
- projectId,
913
- browserType: createdSession?.browserType ?? (options.browserMode ?? "normal"),
914
- createdAt: createdSession?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
915
- inspectUrl: createdSession?.inspectUrl ?? `${this.client.baseUrl}/inspect?session_id=${sessionId}`,
916
- containerId: createdSession?.containerId ?? containerId,
917
- ws: wsUrl ?? createdSession?.ws ?? null,
918
- client: this.client
1157
+ const context = new ContextInfo({
1158
+ id: contextId,
1159
+ status: normalizeContextStatus(result.locked),
1160
+ metadata: result.metadata ?? options.metadata ?? {},
1161
+ createdAt: parseTimestamp(result.created_at)
919
1162
  });
920
- }
921
- async createAsync(payload, options) {
922
- const response = await this.client._post(`${this.client.baseUrl}/instance/v2`, payload);
923
- if (response.status >= 400) {
924
- this.handleCreateError(response);
925
- }
926
- const result = asRecord3(response.data);
927
- const sessionId = getString3(result.session_id);
928
- if (!sessionId) {
929
- throw new APIError("Failed to create session: session_id missing from response", {
930
- statusCode: response.status,
931
- response: response.data
932
- });
933
- }
934
- const projectId = options.projectId ?? this.client.projectId;
935
- const pollIntervalMs = options.pollIntervalMs ?? 1e3;
936
- const pollTimeoutMs = options.pollTimeoutMs ?? 6e5;
937
- const deadline = Date.now() + pollTimeoutMs;
938
- while (Date.now() < deadline) {
939
- const info = await this.get(sessionId, projectId);
940
- if (info.status === "active") {
941
- const wsUrl = await this._getWebSocketDebuggerUrl(sessionId);
942
- getLogger().info(`Session created successfully (async): id=${sessionId}`);
943
- return new SessionInfo({
944
- id: sessionId,
945
- status: "active",
946
- apiKey: this.client.apiKey,
947
- projectId,
948
- browserType: info.browserType || (options.browserMode ?? "normal"),
949
- createdAt: info.createdAt,
950
- inspectUrl: info.inspectUrl,
951
- containerId: info.containerId,
952
- ws: wsUrl ?? info.ws,
953
- client: this.client
954
- });
955
- }
956
- if (info.status === "create_failed") {
957
- this.handleAsyncCreateFailed(payload);
958
- throw new APIError("Session creation failed", {
959
- statusCode: 500,
960
- response: { session_id: sessionId, status: info.status }
961
- });
962
- }
963
- if (info.status === "closed") {
964
- throw new APIError("Session closed before activation", {
965
- statusCode: 500,
966
- response: { session_id: sessionId, status: info.status }
967
- });
968
- }
969
- await delay(pollIntervalMs);
970
- }
971
- throw new TimeoutError(
972
- `Timed out waiting for session ${sessionId} to become active after ${pollTimeoutMs}ms`
973
- );
1163
+ getLogger().info(`Successfully created context '${context.id}'`);
1164
+ return context;
974
1165
  }
975
1166
  /**
976
- * Fetch one session by id.
977
- *
978
- * The upstream API uses POST /instance/session for this lookup, so this SDK method
979
- * intentionally uses POST even though the operation is read-only.
1167
+ * List contexts in the current project.
980
1168
  */
981
- async get(sessionId, projectId) {
982
- const response = await this.client._post(`${this.client.baseUrl}/instance/session`, {
1169
+ async list(options = {}) {
1170
+ const url = `${this.client.baseUrl}/instance/v1/contexts/list-contexts`;
1171
+ const payload = {
983
1172
  api_key: this.client.apiKey,
984
- project_id: projectId ?? this.client.projectId,
985
- session_id: sessionId
986
- });
1173
+ project_id: this.client.projectId,
1174
+ limit: options.limit ?? 20
1175
+ };
1176
+ if (options.status) {
1177
+ payload.status = options.status;
1178
+ }
1179
+ const response = await this.client._post(url, payload);
987
1180
  if (response.status >= 400) {
988
- this.handleGetError(response, sessionId);
1181
+ this.handleError(response, "list");
989
1182
  }
990
- return this.mapSessionInfo(asRecord3(response.data), projectId);
1183
+ const result = asRecord2(response.data);
1184
+ const contexts = Array.isArray(result.contexts) ? result.contexts.map((item) => {
1185
+ const context = asRecord2(item);
1186
+ return new ContextInfo({
1187
+ id: getString2(context.context_id) ?? "",
1188
+ status: normalizeContextStatus(context.locked),
1189
+ createdAt: parseTimestamp(context.created_at),
1190
+ updatedAt: parseTimestamp(context.updated_at),
1191
+ metadata: context.metadata ?? {}
1192
+ });
1193
+ }) : [];
1194
+ getLogger().info(`Retrieved ${contexts.length} contexts`);
1195
+ return new ContextListResponse(contexts);
991
1196
  }
992
1197
  /**
993
- * List sessions for the current project.
1198
+ * Fetch a single context.
994
1199
  */
995
- async list(options = {}) {
996
- const url = `${this.client.baseUrl}/instance/v2/sessions`;
1200
+ async get(contextId) {
1201
+ const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}`;
997
1202
  const payload = {
998
1203
  api_key: this.client.apiKey,
999
- project_id: options.projectId ?? this.client.projectId
1204
+ project_id: this.client.projectId
1000
1205
  };
1001
- if (options.status) {
1002
- payload.status = options.status;
1003
- }
1004
1206
  const response = await this.client._post(url, payload);
1005
1207
  if (response.status >= 400) {
1006
- this.handleListError(response);
1208
+ this.handleError(response, "get", contextId);
1007
1209
  }
1008
- const result = asRecord3(response.data);
1009
- const sessionsData = Array.isArray(result.sessions) ? result.sessions : [];
1010
- const paginationData = asRecord3(result.pagination);
1011
- const sessions = sessionsData.map((item) => this.mapSessionInfo(asRecord3(item), options.projectId));
1012
- const pagination = new PaginationInfo({
1013
- currentPage: getNumber(paginationData.currentPage) ?? 1,
1014
- pageSize: getNumber(paginationData.pageSize) ?? sessions.length,
1015
- totalCount: getNumber(paginationData.totalCount) ?? sessions.length,
1016
- totalPages: getNumber(paginationData.totalPages) ?? 1,
1017
- activeCount: getNumber(paginationData.activeCount) ?? 0,
1018
- closedCount: getNumber(paginationData.closedCount) ?? 0
1210
+ const result = asRecord2(response.data);
1211
+ const context = asRecord2(result.context);
1212
+ const contextInfo = new ContextInfo({
1213
+ id: getString2(context.context_id) ?? contextId,
1214
+ status: normalizeContextStatus(context.locked),
1215
+ metadata: context.metadata ?? {},
1216
+ createdAt: parseTimestamp(context.created_at),
1217
+ updatedAt: parseTimestamp(context.updated_at)
1019
1218
  });
1020
- getLogger().info(
1021
- `Retrieved ${sessions.length} sessions (total: ${pagination.totalCount}, active: ${pagination.activeCount}, closed: ${pagination.closedCount})`
1022
- );
1023
- return new SessionListResponse(sessions, pagination);
1219
+ getLogger().info(`Retrieved context '${contextInfo.id}' (status: ${contextInfo.status})`);
1220
+ return contextInfo;
1024
1221
  }
1025
1222
  /**
1026
- * Delete a browser session.
1223
+ * Fork an existing persistent context into a new context.
1027
1224
  */
1028
- async delete(options) {
1029
- const url = `${this.client.baseUrl}/instance`;
1225
+ async fork(contextId) {
1226
+ const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}/fork`;
1030
1227
  const payload = {
1031
1228
  api_key: this.client.apiKey,
1032
- project_id: options.projectId ?? this.client.projectId,
1033
- session_id: options.sessionId
1229
+ project_id: this.client.projectId
1034
1230
  };
1035
- const response = await this.client._delete(url, payload);
1231
+ const response = await this.client._post(url, payload);
1036
1232
  if (response.status >= 400) {
1037
- this.handleDeleteError(response, options.sessionId);
1233
+ this.handleError(response, "fork", contextId);
1038
1234
  }
1039
- getLogger().info(`Session deleted successfully: ${options.sessionId}`);
1235
+ const result = asRecord2(response.data);
1236
+ const forkedContextId = getString2(result.context_id);
1237
+ if (!forkedContextId) {
1238
+ throw new APIError("Failed to fork context: context_id missing from response", {
1239
+ statusCode: response.status,
1240
+ response: response.data
1241
+ });
1242
+ }
1243
+ const contextInfo = new ContextInfo({
1244
+ id: forkedContextId,
1245
+ status: normalizeContextStatus(result.locked),
1246
+ metadata: result.metadata ?? {},
1247
+ createdAt: parseTimestamp(result.created_at),
1248
+ updatedAt: parseTimestamp(result.updated_at)
1249
+ });
1250
+ getLogger().info(`Successfully forked context '${contextId}' -> '${contextInfo.id}'`);
1251
+ return contextInfo;
1040
1252
  }
1041
1253
  /**
1042
- * List target/page metadata for a session via `/json`.
1254
+ * Delete a persistent context.
1043
1255
  */
1044
- async listTargets(sessionId) {
1045
- const response = await this.client._get(`${this.client.baseUrl}/json`, {
1046
- session_id: sessionId
1047
- });
1256
+ async delete(contextId) {
1257
+ const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}`;
1258
+ const payload = {
1259
+ api_key: this.client.apiKey,
1260
+ project_id: this.client.projectId
1261
+ };
1262
+ const response = await this.client._delete(url, payload);
1048
1263
  if (response.status >= 400) {
1049
- this.handleListTargetsError(response, sessionId);
1264
+ this.handleError(response, "delete", contextId);
1050
1265
  }
1051
- const result = Array.isArray(response.data) ? response.data : [];
1052
- return result.map((item) => this.mapSessionTarget(asRecord3(item)));
1266
+ getLogger().info(`Successfully deleted context '${contextId}'`);
1053
1267
  }
1054
1268
  /**
1055
- * Fetch the debugger WebSocket URL for a session.
1056
- *
1057
- * @internal
1269
+ * Force-release a stuck lock on a context.
1058
1270
  */
1059
- async _getWebSocketDebuggerUrl(sessionId) {
1060
- try {
1061
- const response = await this.client._get(`${this.client.baseUrl}/json/version`, {
1062
- session_id: sessionId
1063
- });
1064
- if (response.status >= 400) {
1065
- getLogger().error("Error getting WebSocket debugger URL:", response.data);
1066
- return null;
1067
- }
1068
- const result = asRecord3(response.data);
1069
- return getString3(result.webSocketDebuggerUrlTransformed) ?? getString3(result.webSocketDebuggerUrl) ?? null;
1070
- } catch (error) {
1071
- getLogger().error("Error getting WebSocket debugger URL:", error);
1072
- return null;
1073
- }
1074
- }
1075
- normalizeProxy(proxy) {
1076
- if (!proxy.server) {
1077
- throw new ValidationError("proxy.server is required when proxy is provided");
1078
- }
1079
- const normalizedProxy = {
1080
- type: proxy.type ?? "external",
1081
- server: proxy.server
1271
+ async forceRelease(contextId) {
1272
+ const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}/force-release`;
1273
+ const payload = {
1274
+ api_key: this.client.apiKey,
1275
+ project_id: this.client.projectId
1082
1276
  };
1083
- if (proxy.username) {
1084
- normalizedProxy.username = proxy.username;
1085
- }
1086
- if (proxy.password) {
1087
- normalizedProxy.password = proxy.password;
1088
- }
1089
- return normalizedProxy;
1090
- }
1091
- handleAsyncCreateFailed(payload) {
1092
- const context = asRecord3(payload.context);
1093
- const contextId = getString3(context.id);
1094
- const contextMode = getString3(context.mode);
1095
- if (contextId && contextMode === "read_write" && payload.weak_lock !== true) {
1096
- throw new ContextLockedError("Context is locked by another active session");
1277
+ const response = await this.client._post(url, payload);
1278
+ if (response.status >= 400) {
1279
+ this.handleError(response, "force_release", contextId);
1097
1280
  }
1281
+ const result = asRecord2(response.data);
1282
+ return {
1283
+ status: getString2(result.status) ?? "unlocked",
1284
+ message: getString2(result.message) ?? "Lock released successfully"
1285
+ };
1098
1286
  }
1099
- handleCreateError(response) {
1100
- const errorData = asRecord3(response.data);
1101
- const errorMessage = getString3(errorData.error) ?? getString3(errorData.message) ?? "Unknown error";
1102
- const errorCode = getString3(errorData.code);
1103
- const metadata = asRecord3(errorData.metadata);
1287
+ handleError(response, operation, contextId) {
1288
+ const errorData = asRecord2(response.data);
1289
+ const errorMessage = getString2(errorData.error) ?? getString2(errorData.message) ?? "Unknown error";
1290
+ const errorCode = getString2(errorData.code);
1291
+ const metadata = asRecord2(errorData.metadata);
1104
1292
  if (response.status === 401) {
1105
- throw new AuthenticationError(
1106
- `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
1107
- );
1108
- }
1109
- if (response.status === 404 && errorCode === "context_not_found") {
1110
- throw new ContextNotFoundError(`Context not found: ${errorMessage}`);
1293
+ throw new AuthenticationError(`Authentication failed: ${errorMessage}`);
1111
1294
  }
1112
1295
  if (response.status === 404) {
1113
- throw new SessionNotFoundError(`Resource not found: ${errorMessage}`);
1296
+ const suffix = contextId ? `: ${contextId}` : "";
1297
+ throw new ContextNotFoundError(`Context not found${suffix}`);
1114
1298
  }
1115
- if (response.status === 409 && errorCode === "context_locked") {
1299
+ if (response.status === 409 && (errorCode === "context_locked" || errorCode === "CONTEXT_FORK_SOURCE_LOCKED")) {
1116
1300
  throw new ContextLockedError(errorMessage, {
1117
- activeSessionId: getString3(metadata.activeSessionId),
1118
- retryAfter: getNumber(metadata.retryAfter)
1301
+ activeSessionId: getString2(metadata.activeSessionId),
1302
+ retryAfter: typeof metadata.retryAfter === "number" ? metadata.retryAfter : void 0
1119
1303
  });
1120
1304
  }
1121
- throw new APIError(`Failed to create session: ${errorMessage}`, {
1122
- statusCode: response.status,
1123
- response: response.data
1124
- });
1125
- }
1126
- handleGetError(response, sessionId) {
1127
- const errorData = asRecord3(response.data);
1128
- const errorMessage = getString3(errorData.error) ?? getString3(errorData.message) ?? "Unknown error";
1129
- if (response.status === 401) {
1130
- throw new AuthenticationError(
1131
- `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
1305
+ if (response.status === 409 && errorCode === "session_active") {
1306
+ throw new APIError(
1307
+ `Cannot ${operation} context: ${errorMessage} (session is active)`,
1308
+ {
1309
+ statusCode: response.status,
1310
+ response: response.data
1311
+ }
1132
1312
  );
1133
1313
  }
1134
- if (response.status === 404) {
1135
- throw new SessionNotFoundError(
1136
- `Session not found: ${errorMessage}. Session ID '${sessionId}' may not exist in this project.`
1314
+ if (response.status === 500) {
1315
+ const details = getString2(errorData.details);
1316
+ throw new APIError(
1317
+ `Internal server error (500) during ${operation}: ${errorMessage}${details ? `. Details: ${details}` : ""}`,
1318
+ {
1319
+ statusCode: response.status,
1320
+ response: response.data
1321
+ }
1137
1322
  );
1138
1323
  }
1139
- throw new APIError(`Failed to get session: ${errorMessage}`, {
1140
- statusCode: response.status,
1141
- response: response.data
1142
- });
1143
- }
1144
- handleListError(response) {
1145
- const errorData = asRecord3(response.data);
1146
- const errorMessage = getString3(errorData.error) ?? getString3(errorData.message) ?? "Unknown error";
1147
- if (response.status === 401) {
1148
- throw new AuthenticationError(
1149
- `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
1324
+ if (response.status === 502) {
1325
+ throw new APIError(
1326
+ `Server gateway error (502) during ${operation}: The service may be temporarily unavailable. Please retry.`,
1327
+ {
1328
+ statusCode: response.status,
1329
+ response: response.data
1330
+ }
1150
1331
  );
1151
1332
  }
1152
- throw new APIError(`Failed to list sessions: ${errorMessage}`, {
1153
- statusCode: response.status,
1154
- response: response.data
1155
- });
1156
- }
1157
- handleListTargetsError(response, sessionId) {
1158
- const errorData = asRecord3(response.data);
1159
- const errorMessage = getString3(errorData.error) ?? getString3(errorData.message) ?? "Unknown error";
1160
- if (response.status === 401) {
1161
- throw new AuthenticationError(
1162
- `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
1333
+ if (response.status === 503) {
1334
+ throw new APIError(
1335
+ `Service unavailable (503) during ${operation}: The service is temporarily unavailable. Please retry later.`,
1336
+ {
1337
+ statusCode: response.status,
1338
+ response: response.data
1339
+ }
1163
1340
  );
1164
1341
  }
1165
- if (response.status === 404) {
1166
- throw new SessionNotFoundError(
1167
- `Session not found: ${errorMessage}. Session ID '${sessionId}' may not exist in this project.`
1342
+ if (response.status === 504) {
1343
+ throw new APIError(
1344
+ `Gateway timeout (504) during ${operation}: The request timed out. Please retry.`,
1345
+ {
1346
+ statusCode: response.status,
1347
+ response: response.data
1348
+ }
1168
1349
  );
1169
1350
  }
1170
- throw new APIError(`Failed to list session targets: ${errorMessage}`, {
1351
+ throw new APIError(`Failed to ${operation} context: ${errorMessage}`, {
1171
1352
  statusCode: response.status,
1172
1353
  response: response.data
1173
1354
  });
1174
1355
  }
1175
- handleDeleteError(response, sessionId) {
1176
- const errorData = asRecord3(response.data);
1177
- const errorMessage = getString3(errorData.error) ?? getString3(errorData.message) ?? "Unknown error";
1178
- if (response.status === 401) {
1179
- throw new AuthenticationError(
1180
- `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
1181
- );
1356
+ };
1357
+
1358
+ // src/extensions.ts
1359
+ var import_promises = require("fs/promises");
1360
+ var import_node_path = __toESM(require("path"));
1361
+ function asRecord3(value) {
1362
+ return typeof value === "object" && value !== null ? value : {};
1363
+ }
1364
+ function getString3(value) {
1365
+ return typeof value === "string" ? value : void 0;
1366
+ }
1367
+ var ExtensionInfo = class {
1368
+ constructor(shape) {
1369
+ this.id = shape.id;
1370
+ this.name = shape.name;
1371
+ this.projectId = shape.projectId;
1372
+ this.createdAt = shape.createdAt;
1373
+ this.updatedAt = shape.updatedAt;
1374
+ }
1375
+ };
1376
+ var ExtensionsResource = class {
1377
+ constructor(client) {
1378
+ this.client = client;
1379
+ }
1380
+ async upload(filePath, options = {}) {
1381
+ const fileBuffer = await (0, import_promises.readFile)(filePath);
1382
+ const form = new FormData();
1383
+ const usedProjectId = options.projectId ?? this.client.projectId;
1384
+ form.append("project_id", usedProjectId);
1385
+ if (options.name) {
1386
+ form.append("name", options.name);
1182
1387
  }
1183
- if (response.status === 404) {
1184
- throw new SessionNotFoundError(
1185
- `Session not found: ${errorMessage}. Session ID '${sessionId}' may have already been deleted or never existed.`
1186
- );
1388
+ form.append(
1389
+ "file",
1390
+ new Blob([fileBuffer], { type: "application/octet-stream" }),
1391
+ import_node_path.default.basename(filePath)
1392
+ );
1393
+ const response = await this.client._post(
1394
+ `${this.client.baseUrl}/instance/v1/extension/upload`,
1395
+ form,
1396
+ {
1397
+ headers: {
1398
+ project_id: usedProjectId
1399
+ }
1400
+ }
1401
+ );
1402
+ if (response.status >= 400) {
1403
+ this.handleError("upload extension", response);
1187
1404
  }
1188
- throw new APIError(`Failed to delete session: ${errorMessage}`, {
1189
- statusCode: response.status,
1190
- response: response.data
1405
+ getLogger().info(`Extension uploaded successfully: ${filePath}`);
1406
+ return this.parseInfo(response.data);
1407
+ }
1408
+ async list(options = {}) {
1409
+ const response = await this.client._post(`${this.client.baseUrl}/instance/v1/extension/list`, {
1410
+ project_id: options.projectId ?? this.client.projectId,
1411
+ limit: options.limit ?? 20,
1412
+ offset: options.offset ?? 0
1191
1413
  });
1414
+ if (response.status >= 400) {
1415
+ this.handleError("list extensions", response);
1416
+ }
1417
+ const result = asRecord3(response.data);
1418
+ const items = Array.isArray(result.data) ? result.data : [];
1419
+ return items.map((item) => this.parseInfo(item));
1192
1420
  }
1193
- mapSessionInfo(raw, projectId) {
1194
- const sessionId = getString3(raw.session_id) ?? getString3(raw.id) ?? "";
1195
- return new SessionInfo({
1196
- id: sessionId,
1197
- status: getString3(raw.status) ?? "active",
1198
- apiKey: getString3(raw.api_key) ?? this.client.apiKey,
1199
- projectId: getString3(raw.project_id) ?? (projectId ?? this.client.projectId),
1200
- regionId: getString3(raw.region_id) ?? this.client.selectedRegion ?? this.client.region ?? null,
1201
- browserType: getString3(raw.browser_type) ?? getString3(raw.browser_mode) ?? "normal",
1202
- createdAt: getString3(raw.created_at) ?? "",
1203
- inspectUrl: getString3(raw.inspect_url) ?? `${this.client.baseUrl}/inspect?session_id=${sessionId}`,
1204
- containerId: getString3(raw.container_id) ?? null,
1205
- ws: getString3(raw.ws) ?? null,
1206
- client: this.client
1421
+ async get(extensionId, projectId) {
1422
+ const response = await this.client._post(`${this.client.baseUrl}/instance/v1/extension/info`, {
1423
+ project_id: projectId ?? this.client.projectId,
1424
+ extension_id: extensionId
1207
1425
  });
1426
+ if (response.status >= 400) {
1427
+ this.handleError("get extension", response);
1428
+ }
1429
+ return this.parseInfo(response.data);
1208
1430
  }
1209
- mapSessionTarget(raw) {
1210
- return new SessionTargetInfo({
1211
- id: getString3(raw.id) ?? "",
1212
- title: getString3(raw.title) ?? "",
1213
- type: getString3(raw.type) ?? "",
1214
- url: getString3(raw.url) ?? "",
1215
- description: getString3(raw.description) ?? "",
1216
- inspectUrl: getString3(raw.inspectUrl) ?? getString3(raw.inspect_url) ?? null,
1217
- webSocketDebuggerUrl: getString3(raw.webSocketDebuggerUrl) ?? getString3(raw.web_socket_debugger_url) ?? null,
1218
- webSocketDebuggerUrlTransformed: getString3(raw.webSocketDebuggerUrlTransformed) ?? getString3(raw.web_socket_debugger_url_transformed) ?? null
1431
+ async delete(extensionId) {
1432
+ const response = await this.client._delete(
1433
+ `${this.client.baseUrl}/instance/v1/extension/${extensionId}`
1434
+ );
1435
+ if (response.status >= 400) {
1436
+ this.handleError("delete extension", response);
1437
+ }
1438
+ getLogger().info(`Extension deleted successfully: ${extensionId}`);
1439
+ }
1440
+ parseInfo(data) {
1441
+ const item = asRecord3(data);
1442
+ return new ExtensionInfo({
1443
+ id: getString3(item.id) ?? "",
1444
+ name: getString3(item.name) ?? "",
1445
+ projectId: getString3(item.project_id) ?? "",
1446
+ createdAt: getString3(item.created_at),
1447
+ updatedAt: getString3(item.updated_at)
1219
1448
  });
1220
1449
  }
1221
- async _getCreatedSession(sessionId, projectId) {
1222
- try {
1223
- return await this.get(sessionId, projectId);
1224
- } catch (error) {
1225
- getLogger().warn(
1226
- `Failed to fetch created session ${sessionId}; falling back to locally derived session fields:`,
1227
- error
1450
+ handleError(action, response) {
1451
+ const errorData = asRecord3(response.data);
1452
+ const errorMessage = getString3(errorData.message) ?? getString3(errorData.error) ?? "Unknown error";
1453
+ if (response.status === 401) {
1454
+ throw new AuthenticationError(
1455
+ `Authentication failed: ${errorMessage}. Please check your API key and project ID.`
1228
1456
  );
1229
- return null;
1230
1457
  }
1458
+ throw new APIError(`Failed to ${action}: ${errorMessage}`, {
1459
+ statusCode: response.status,
1460
+ response: response.data
1461
+ });
1231
1462
  }
1232
1463
  };
1233
1464
 
@@ -1498,11 +1729,12 @@ var Lexmount = class {
1498
1729
  };
1499
1730
 
1500
1731
  // src/index.ts
1501
- var VERSION = "0.5.4";
1732
+ var VERSION = "0.5.5";
1502
1733
  // Annotate the CommonJS export names for ESM import in node:
1503
1734
  0 && (module.exports = {
1504
1735
  APIError,
1505
1736
  AuthenticationError,
1737
+ CDPClient,
1506
1738
  ContextInfo,
1507
1739
  ContextListResponse,
1508
1740
  ContextLockedError,
@@ -1527,9 +1759,12 @@ var VERSION = "0.5.4";
1527
1759
  TimeoutError,
1528
1760
  VERSION,
1529
1761
  ValidationError,
1762
+ connectOverCDP,
1763
+ deregisterIntegratedAuthCallback,
1530
1764
  disableLogging,
1531
1765
  enableLogging,
1532
1766
  getLogger,
1767
+ registerIntegratedAuthCallback,
1533
1768
  setLogLevel
1534
1769
  });
1535
1770
  //# sourceMappingURL=index.js.map