lexmount 0.2.7 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,7 +1,5 @@
1
1
  // src/client.ts
2
- import {
3
- isAxiosError
4
- } from "axios";
2
+ import { isAxiosError } from "axios";
5
3
  import axios from "axios";
6
4
  import * as dotenv from "dotenv";
7
5
 
@@ -556,6 +554,11 @@ function normalizeContextMode(mode) {
556
554
  }
557
555
  return "read_only";
558
556
  }
557
+ function delay(ms) {
558
+ return new Promise((resolve) => {
559
+ setTimeout(resolve, ms);
560
+ });
561
+ }
559
562
  var PaginationInfo = class {
560
563
  constructor(shape) {
561
564
  this.currentPage = shape.currentPage;
@@ -787,7 +790,6 @@ var SessionsResource = class {
787
790
  * Create a new browser session.
788
791
  */
789
792
  async create(options = {}) {
790
- const url = `${this.client.baseUrl}/instance`;
791
793
  const payload = {
792
794
  api_key: this.client.apiKey,
793
795
  project_id: options.projectId ?? this.client.projectId,
@@ -811,6 +813,13 @@ var SessionsResource = class {
811
813
  if (options.proxy) {
812
814
  payload.proxy = this.normalizeProxy(options.proxy);
813
815
  }
816
+ if (options.weakLock) {
817
+ payload.weak_lock = true;
818
+ }
819
+ if (options.asyncCreate !== false) {
820
+ return this.createAsync(payload, options);
821
+ }
822
+ const url = `${this.client.baseUrl}/instance`;
814
823
  const response = await this.client._post(url, payload);
815
824
  if (response.status >= 400) {
816
825
  this.handleCreateError(response);
@@ -841,6 +850,59 @@ var SessionsResource = class {
841
850
  client: this.client
842
851
  });
843
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
+ throw new APIError("Session creation failed", {
890
+ statusCode: 500,
891
+ response: { session_id: sessionId, status: info.status }
892
+ });
893
+ }
894
+ if (info.status === "closed") {
895
+ throw new APIError("Session closed before activation", {
896
+ statusCode: 500,
897
+ response: { session_id: sessionId, status: info.status }
898
+ });
899
+ }
900
+ await delay(pollIntervalMs);
901
+ }
902
+ throw new TimeoutError(
903
+ `Timed out waiting for session ${sessionId} to become active after ${pollTimeoutMs}ms`
904
+ );
905
+ }
844
906
  /**
845
907
  * Fetch one session by id.
846
908
  *
@@ -1095,6 +1157,7 @@ var SessionsResource = class {
1095
1157
  dotenv.config();
1096
1158
  var Lexmount = class {
1097
1159
  constructor(config2 = {}) {
1160
+ this.regionResolutionAttempted = false;
1098
1161
  this.apiKey = config2.apiKey ?? process.env.LEXMOUNT_API_KEY ?? "";
1099
1162
  if (!this.apiKey) {
1100
1163
  throw new ValidationError(
@@ -1108,6 +1171,7 @@ var Lexmount = class {
1108
1171
  );
1109
1172
  }
1110
1173
  this.baseUrl = (config2.baseUrl ?? process.env.LEXMOUNT_BASE_URL ?? "https://api.lexmount.cn").replace(/\/$/, "");
1174
+ this.region = config2.region;
1111
1175
  if (config2.logLevel) {
1112
1176
  setLogLevel(config2.logLevel);
1113
1177
  }
@@ -1125,6 +1189,7 @@ var Lexmount = class {
1125
1189
  * @internal
1126
1190
  */
1127
1191
  async _post(url, data, config2 = {}) {
1192
+ await this.ensureRegionResolved();
1128
1193
  return this.request("POST", url, { ...config2, data });
1129
1194
  }
1130
1195
  /**
@@ -1133,6 +1198,7 @@ var Lexmount = class {
1133
1198
  * @internal
1134
1199
  */
1135
1200
  async _get(url, params, config2 = {}) {
1201
+ await this.ensureRegionResolved();
1136
1202
  return this.request("GET", url, { ...config2, params });
1137
1203
  }
1138
1204
  /**
@@ -1141,8 +1207,51 @@ var Lexmount = class {
1141
1207
  * @internal
1142
1208
  */
1143
1209
  async _delete(url, data, config2 = {}) {
1210
+ await this.ensureRegionResolved();
1144
1211
  return this.request("DELETE", url, { ...config2, data });
1145
1212
  }
1213
+ async regionInfo() {
1214
+ await this.ensureRegionResolved();
1215
+ return {
1216
+ requestedRegion: this.region,
1217
+ selectedRegion: this.selectedRegion,
1218
+ baseUrl: this.baseUrl,
1219
+ endpointBaseUrl: this.endpointBaseUrl,
1220
+ host: this.selectedHost,
1221
+ resolved: this.regionResolutionAttempted,
1222
+ usingRegion: this.selectedRegion !== void 0
1223
+ };
1224
+ }
1225
+ async catalogInfo() {
1226
+ try {
1227
+ const response = await this.httpClient.get(
1228
+ `${this.baseUrl}/v1/regions/catalog`
1229
+ );
1230
+ if (response.status !== 200) {
1231
+ return {
1232
+ baseUrl: this.baseUrl,
1233
+ available: false,
1234
+ statusCode: response.status,
1235
+ regions: []
1236
+ };
1237
+ }
1238
+ const regions = Array.isArray(response.data?.regions) ? response.data.regions : [];
1239
+ return {
1240
+ baseUrl: this.baseUrl,
1241
+ available: Array.isArray(response.data?.regions),
1242
+ statusCode: response.status,
1243
+ regions,
1244
+ error: Array.isArray(response.data?.regions) ? void 0 : "invalid catalog regions"
1245
+ };
1246
+ } catch (error) {
1247
+ return {
1248
+ baseUrl: this.baseUrl,
1249
+ available: false,
1250
+ regions: [],
1251
+ error: error instanceof Error ? error.message : String(error)
1252
+ };
1253
+ }
1254
+ }
1146
1255
  /**
1147
1256
  * Close the client.
1148
1257
  *
@@ -1160,7 +1269,8 @@ var Lexmount = class {
1160
1269
  async request(method, url, config2) {
1161
1270
  const logger2 = getLogger();
1162
1271
  const startTime = Date.now();
1163
- logger2.debug(`${method} request to ${url}`);
1272
+ const requestUrl = this.rewriteUrl(url);
1273
+ logger2.debug(`${method} request to ${requestUrl}`);
1164
1274
  try {
1165
1275
  const headers = {
1166
1276
  api_key: this.apiKey,
@@ -1170,12 +1280,14 @@ var Lexmount = class {
1170
1280
  };
1171
1281
  const response = await this.httpClient.request({
1172
1282
  method,
1173
- url,
1283
+ url: requestUrl,
1174
1284
  ...config2,
1175
1285
  headers
1176
1286
  });
1177
1287
  const elapsed = Date.now() - startTime;
1178
- logger2.debug(`${method} response: status=${response.status}, duration=${elapsed.toFixed(2)}ms`);
1288
+ logger2.debug(
1289
+ `${method} response: status=${response.status}, duration=${elapsed.toFixed(2)}ms`
1290
+ );
1179
1291
  return response;
1180
1292
  } catch (error) {
1181
1293
  const elapsed = Date.now() - startTime;
@@ -1214,10 +1326,101 @@ var Lexmount = class {
1214
1326
  "Content-Type": "application/json"
1215
1327
  };
1216
1328
  }
1329
+ async ensureRegionResolved() {
1330
+ if (this.regionResolutionAttempted) {
1331
+ return;
1332
+ }
1333
+ if (!this.regionResolutionPromise) {
1334
+ this.regionResolutionPromise = this.resolveRegion().finally(() => {
1335
+ this.regionResolutionAttempted = true;
1336
+ this.regionResolutionPromise = void 0;
1337
+ });
1338
+ }
1339
+ await this.regionResolutionPromise;
1340
+ }
1341
+ async resolveRegion() {
1342
+ const logger2 = getLogger();
1343
+ try {
1344
+ const response = await this.httpClient.get(
1345
+ `${this.baseUrl}/v1/regions/catalog`
1346
+ );
1347
+ if (response.status !== 200 || !Array.isArray(response.data?.regions)) {
1348
+ logger2.debug(
1349
+ `Region catalog unavailable, falling back to baseUrl: status=${response.status}`
1350
+ );
1351
+ return;
1352
+ }
1353
+ const selected = this.selectCatalogRegion(response.data.regions);
1354
+ if (!selected) {
1355
+ if (this.region) {
1356
+ logger2.warn(`Requested region not found in catalog: ${this.region}`);
1357
+ } else {
1358
+ logger2.debug("No default catalog region, falling back to baseUrl");
1359
+ }
1360
+ return;
1361
+ }
1362
+ const endpointBaseUrl = await this.probeRegionHost(selected);
1363
+ if (!endpointBaseUrl) {
1364
+ logger2.debug("Region probe failed, falling back to baseUrl");
1365
+ return;
1366
+ }
1367
+ this.selectedRegion = selected.region_id;
1368
+ this.selectedHost = selected.host;
1369
+ this.endpointBaseUrl = endpointBaseUrl;
1370
+ } catch (error) {
1371
+ logger2.debug("Region resolution failed, falling back to baseUrl:", error);
1372
+ }
1373
+ }
1374
+ selectCatalogRegion(regions) {
1375
+ if (this.region) {
1376
+ return regions.find((region) => region.region_id === this.region);
1377
+ }
1378
+ const defaultRegions = regions.filter((region) => region.default === true);
1379
+ return defaultRegions.length === 1 ? defaultRegions[0] : void 0;
1380
+ }
1381
+ async probeRegionHost(region) {
1382
+ const logger2 = getLogger();
1383
+ const host = String(region.host || "").trim();
1384
+ if (!host) {
1385
+ return void 0;
1386
+ }
1387
+ if (!this.isAllowedCatalogHost(host)) {
1388
+ logger2.warn(`Ignoring invalid region catalog host: ${host}`);
1389
+ return void 0;
1390
+ }
1391
+ const endpointBaseUrl = `https://${host}`;
1392
+ const probePath = String(region.probe_path || "/v1/region/probe").replace(/^(?!\/)/, "/");
1393
+ try {
1394
+ const response = await this.httpClient.get(`${endpointBaseUrl}${probePath}`);
1395
+ return response.status === 200 ? endpointBaseUrl : void 0;
1396
+ } catch {
1397
+ return void 0;
1398
+ }
1399
+ }
1400
+ isAllowedCatalogHost(host) {
1401
+ let parsed;
1402
+ try {
1403
+ parsed = new URL(`https://${host}`);
1404
+ } catch {
1405
+ return false;
1406
+ }
1407
+ if (parsed.username || parsed.password || parsed.port || parsed.pathname !== "/" || parsed.search || parsed.hash) {
1408
+ return false;
1409
+ }
1410
+ const hostname = parsed.hostname.toLowerCase();
1411
+ const allowedDomains = ["lexmount.cn", "lexmount.com", "lexmount.net"];
1412
+ return allowedDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
1413
+ }
1414
+ rewriteUrl(url) {
1415
+ if (!this.endpointBaseUrl || !url.startsWith(this.baseUrl)) {
1416
+ return url;
1417
+ }
1418
+ return `${this.endpointBaseUrl}${url.slice(this.baseUrl.length)}`;
1419
+ }
1217
1420
  };
1218
1421
 
1219
1422
  // src/index.ts
1220
- var VERSION = "0.2.5";
1423
+ var VERSION = "0.5.2";
1221
1424
  export {
1222
1425
  APIError,
1223
1426
  AuthenticationError,