lexmount 0.5.4 → 0.5.6

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