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