lexmount 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +493 -0
- package/dist/index.d.mts +492 -0
- package/dist/index.d.ts +492 -0
- package/dist/index.js +903 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +846 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +75 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import {
|
|
3
|
+
isAxiosError
|
|
4
|
+
} from "axios";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import * as dotenv from "dotenv";
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var LexmountError = class extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var AuthenticationError = class extends LexmountError {
|
|
16
|
+
};
|
|
17
|
+
var SessionNotFoundError = class extends LexmountError {
|
|
18
|
+
};
|
|
19
|
+
var ContextNotFoundError = class extends LexmountError {
|
|
20
|
+
};
|
|
21
|
+
var ContextLockedError = class extends LexmountError {
|
|
22
|
+
constructor(message, options = {}) {
|
|
23
|
+
const details = [];
|
|
24
|
+
if (options.activeSessionId) {
|
|
25
|
+
details.push(`locked by session: ${options.activeSessionId}`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof options.retryAfter === "number") {
|
|
28
|
+
details.push(`retry after ${options.retryAfter}s`);
|
|
29
|
+
}
|
|
30
|
+
super(details.length > 0 ? `${message} (${details.join(", ")})` : message);
|
|
31
|
+
this.activeSessionId = options.activeSessionId;
|
|
32
|
+
this.retryAfter = options.retryAfter;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var APIError = class extends LexmountError {
|
|
36
|
+
constructor(message, options = {}) {
|
|
37
|
+
super(
|
|
38
|
+
typeof options.statusCode === "number" ? `${message} (HTTP ${options.statusCode})` : message
|
|
39
|
+
);
|
|
40
|
+
this.statusCode = options.statusCode;
|
|
41
|
+
this.response = options.response;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var NetworkError = class extends LexmountError {
|
|
45
|
+
};
|
|
46
|
+
var ValidationError = class extends LexmountError {
|
|
47
|
+
};
|
|
48
|
+
var TimeoutError = class extends LexmountError {
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/logging.ts
|
|
52
|
+
var LOGGER_NAME = "lexmount";
|
|
53
|
+
var LOG_LEVEL_PRIORITY = {
|
|
54
|
+
DEBUG: 10,
|
|
55
|
+
INFO: 20,
|
|
56
|
+
WARNING: 30,
|
|
57
|
+
ERROR: 40,
|
|
58
|
+
CRITICAL: 50,
|
|
59
|
+
SILENT: 60
|
|
60
|
+
};
|
|
61
|
+
var currentLevel = "WARNING";
|
|
62
|
+
function isLogLevel(value) {
|
|
63
|
+
return value in LOG_LEVEL_PRIORITY;
|
|
64
|
+
}
|
|
65
|
+
function formatTimestamp(date) {
|
|
66
|
+
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
67
|
+
}
|
|
68
|
+
function shouldLog(level) {
|
|
69
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[currentLevel];
|
|
70
|
+
}
|
|
71
|
+
function emit(level, args) {
|
|
72
|
+
if (!shouldLog(level)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const prefix = `${formatTimestamp(/* @__PURE__ */ new Date())} - ${LOGGER_NAME} - ${level} -`;
|
|
76
|
+
if (level === "ERROR" || level === "CRITICAL") {
|
|
77
|
+
console.error(prefix, ...args);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (level === "WARNING") {
|
|
81
|
+
console.warn(prefix, ...args);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (level === "INFO") {
|
|
85
|
+
console.info(prefix, ...args);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.debug(prefix, ...args);
|
|
89
|
+
}
|
|
90
|
+
var LexmountLogger = class {
|
|
91
|
+
/**
|
|
92
|
+
* Current SDK log level.
|
|
93
|
+
*/
|
|
94
|
+
get level() {
|
|
95
|
+
return currentLevel;
|
|
96
|
+
}
|
|
97
|
+
debug(...args) {
|
|
98
|
+
emit("DEBUG", args);
|
|
99
|
+
}
|
|
100
|
+
info(...args) {
|
|
101
|
+
emit("INFO", args);
|
|
102
|
+
}
|
|
103
|
+
warn(...args) {
|
|
104
|
+
emit("WARNING", args);
|
|
105
|
+
}
|
|
106
|
+
error(...args) {
|
|
107
|
+
emit("ERROR", args);
|
|
108
|
+
}
|
|
109
|
+
critical(...args) {
|
|
110
|
+
emit("CRITICAL", args);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var logger = new LexmountLogger();
|
|
114
|
+
function getLogger() {
|
|
115
|
+
return logger;
|
|
116
|
+
}
|
|
117
|
+
function setLogLevel(level) {
|
|
118
|
+
if (typeof level !== "string" || level.trim() === "") {
|
|
119
|
+
throw new ValidationError(
|
|
120
|
+
"Invalid log level. Valid levels are: DEBUG, INFO, WARNING, ERROR, CRITICAL, SILENT"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const normalizedLevel = level.toUpperCase();
|
|
124
|
+
if (!isLogLevel(normalizedLevel)) {
|
|
125
|
+
throw new ValidationError(
|
|
126
|
+
`Invalid log level: ${level}. Valid levels are: DEBUG, INFO, WARNING, ERROR, CRITICAL, SILENT`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
currentLevel = normalizedLevel;
|
|
130
|
+
logger.debug(`Log level set to ${normalizedLevel}`);
|
|
131
|
+
}
|
|
132
|
+
function disableLogging() {
|
|
133
|
+
currentLevel = "SILENT";
|
|
134
|
+
}
|
|
135
|
+
function enableLogging(level = "INFO") {
|
|
136
|
+
setLogLevel(level);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/contexts.ts
|
|
140
|
+
function asRecord(value) {
|
|
141
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
142
|
+
}
|
|
143
|
+
function getString(value) {
|
|
144
|
+
return typeof value === "string" ? value : void 0;
|
|
145
|
+
}
|
|
146
|
+
function parseTimestamp(value) {
|
|
147
|
+
if (typeof value === "string" && value.length > 0) {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
151
|
+
return new Date(value).toISOString();
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function normalizeContextStatus(value) {
|
|
156
|
+
if (value === true || value === "locked") {
|
|
157
|
+
return "locked";
|
|
158
|
+
}
|
|
159
|
+
if (value === false || value === "available") {
|
|
160
|
+
return "available";
|
|
161
|
+
}
|
|
162
|
+
return typeof value === "string" ? value : "available";
|
|
163
|
+
}
|
|
164
|
+
var ContextInfo = class {
|
|
165
|
+
constructor(options) {
|
|
166
|
+
this.id = options.id;
|
|
167
|
+
this.status = options.status;
|
|
168
|
+
this.metadata = options.metadata ?? {};
|
|
169
|
+
this.createdAt = options.createdAt ?? null;
|
|
170
|
+
this.updatedAt = options.updatedAt ?? null;
|
|
171
|
+
}
|
|
172
|
+
get createdAtDate() {
|
|
173
|
+
if (!this.createdAt) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const date = new Date(this.createdAt);
|
|
177
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
178
|
+
}
|
|
179
|
+
get updatedAtDate() {
|
|
180
|
+
if (!this.updatedAt) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const date = new Date(this.updatedAt);
|
|
184
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
185
|
+
}
|
|
186
|
+
isLocked() {
|
|
187
|
+
return this.status === "locked";
|
|
188
|
+
}
|
|
189
|
+
isAvailable() {
|
|
190
|
+
return this.status === "available";
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var ContextListResponse = class {
|
|
194
|
+
constructor(data) {
|
|
195
|
+
this.data = data;
|
|
196
|
+
}
|
|
197
|
+
get length() {
|
|
198
|
+
return this.data.length;
|
|
199
|
+
}
|
|
200
|
+
[Symbol.iterator]() {
|
|
201
|
+
return this.data[Symbol.iterator]();
|
|
202
|
+
}
|
|
203
|
+
at(index) {
|
|
204
|
+
return this.data[index];
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var ContextsResource = class {
|
|
208
|
+
constructor(client) {
|
|
209
|
+
this.client = client;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Create a new persistent context.
|
|
213
|
+
*/
|
|
214
|
+
async create(options = {}) {
|
|
215
|
+
const url = `${this.client.baseUrl}/instance/v1/contexts/create-context`;
|
|
216
|
+
const payload = {
|
|
217
|
+
api_key: this.client.apiKey,
|
|
218
|
+
project_id: this.client.projectId
|
|
219
|
+
};
|
|
220
|
+
if (options.metadata) {
|
|
221
|
+
payload.metadata = options.metadata;
|
|
222
|
+
}
|
|
223
|
+
const response = await this.client._post(url, payload);
|
|
224
|
+
if (response.status >= 400) {
|
|
225
|
+
this.handleError(response, "create");
|
|
226
|
+
}
|
|
227
|
+
const result = asRecord(response.data);
|
|
228
|
+
const contextId = getString(result.context_id);
|
|
229
|
+
if (!contextId) {
|
|
230
|
+
throw new APIError("Failed to create context: context_id missing from response", {
|
|
231
|
+
statusCode: response.status,
|
|
232
|
+
response: response.data
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const context = new ContextInfo({
|
|
236
|
+
id: contextId,
|
|
237
|
+
status: normalizeContextStatus(result.locked),
|
|
238
|
+
metadata: result.metadata ?? options.metadata ?? {},
|
|
239
|
+
createdAt: parseTimestamp(result.created_at)
|
|
240
|
+
});
|
|
241
|
+
getLogger().info(`Successfully created context '${context.id}'`);
|
|
242
|
+
return context;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* List contexts in the current project.
|
|
246
|
+
*/
|
|
247
|
+
async list(options = {}) {
|
|
248
|
+
const url = `${this.client.baseUrl}/instance/v1/contexts/list-contexts`;
|
|
249
|
+
const payload = {
|
|
250
|
+
api_key: this.client.apiKey,
|
|
251
|
+
project_id: this.client.projectId,
|
|
252
|
+
limit: options.limit ?? 20
|
|
253
|
+
};
|
|
254
|
+
if (options.status) {
|
|
255
|
+
payload.status = options.status;
|
|
256
|
+
}
|
|
257
|
+
const response = await this.client._post(url, payload);
|
|
258
|
+
if (response.status >= 400) {
|
|
259
|
+
this.handleError(response, "list");
|
|
260
|
+
}
|
|
261
|
+
const result = asRecord(response.data);
|
|
262
|
+
const contexts = Array.isArray(result.contexts) ? result.contexts.map((item) => {
|
|
263
|
+
const context = asRecord(item);
|
|
264
|
+
return new ContextInfo({
|
|
265
|
+
id: getString(context.context_id) ?? "",
|
|
266
|
+
status: normalizeContextStatus(context.locked),
|
|
267
|
+
createdAt: parseTimestamp(context.created_at),
|
|
268
|
+
updatedAt: parseTimestamp(context.updated_at),
|
|
269
|
+
metadata: context.metadata ?? {}
|
|
270
|
+
});
|
|
271
|
+
}) : [];
|
|
272
|
+
getLogger().info(`Retrieved ${contexts.length} contexts`);
|
|
273
|
+
return new ContextListResponse(contexts);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Fetch a single context.
|
|
277
|
+
*/
|
|
278
|
+
async get(contextId) {
|
|
279
|
+
const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}`;
|
|
280
|
+
const payload = {
|
|
281
|
+
api_key: this.client.apiKey,
|
|
282
|
+
project_id: this.client.projectId
|
|
283
|
+
};
|
|
284
|
+
const response = await this.client._post(url, payload);
|
|
285
|
+
if (response.status >= 400) {
|
|
286
|
+
this.handleError(response, "get", contextId);
|
|
287
|
+
}
|
|
288
|
+
const result = asRecord(response.data);
|
|
289
|
+
const context = asRecord(result.context);
|
|
290
|
+
const contextInfo = new ContextInfo({
|
|
291
|
+
id: getString(context.context_id) ?? contextId,
|
|
292
|
+
status: normalizeContextStatus(context.locked),
|
|
293
|
+
metadata: context.metadata ?? {},
|
|
294
|
+
createdAt: parseTimestamp(context.created_at),
|
|
295
|
+
updatedAt: parseTimestamp(context.updated_at)
|
|
296
|
+
});
|
|
297
|
+
getLogger().info(`Retrieved context '${contextInfo.id}' (status: ${contextInfo.status})`);
|
|
298
|
+
return contextInfo;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Delete a persistent context.
|
|
302
|
+
*/
|
|
303
|
+
async delete(contextId) {
|
|
304
|
+
const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}`;
|
|
305
|
+
const payload = {
|
|
306
|
+
api_key: this.client.apiKey,
|
|
307
|
+
project_id: this.client.projectId
|
|
308
|
+
};
|
|
309
|
+
const response = await this.client._delete(url, payload);
|
|
310
|
+
if (response.status >= 400) {
|
|
311
|
+
this.handleError(response, "delete", contextId);
|
|
312
|
+
}
|
|
313
|
+
getLogger().info(`Successfully deleted context '${contextId}'`);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Force-release a stuck lock on a context.
|
|
317
|
+
*/
|
|
318
|
+
async forceRelease(contextId) {
|
|
319
|
+
const url = `${this.client.baseUrl}/instance/v1/contexts/${contextId}/force-release`;
|
|
320
|
+
const payload = {
|
|
321
|
+
api_key: this.client.apiKey,
|
|
322
|
+
project_id: this.client.projectId
|
|
323
|
+
};
|
|
324
|
+
const response = await this.client._post(url, payload);
|
|
325
|
+
if (response.status >= 400) {
|
|
326
|
+
this.handleError(response, "force_release", contextId);
|
|
327
|
+
}
|
|
328
|
+
const result = asRecord(response.data);
|
|
329
|
+
return {
|
|
330
|
+
status: getString(result.status) ?? "unlocked",
|
|
331
|
+
message: getString(result.message) ?? "Lock released successfully"
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
handleError(response, operation, contextId) {
|
|
335
|
+
const errorData = asRecord(response.data);
|
|
336
|
+
const errorMessage = getString(errorData.error) ?? getString(errorData.message) ?? "Unknown error";
|
|
337
|
+
const errorCode = getString(errorData.code);
|
|
338
|
+
const metadata = asRecord(errorData.metadata);
|
|
339
|
+
if (response.status === 401) {
|
|
340
|
+
throw new AuthenticationError(`Authentication failed: ${errorMessage}`);
|
|
341
|
+
}
|
|
342
|
+
if (response.status === 404) {
|
|
343
|
+
const suffix = contextId ? `: ${contextId}` : "";
|
|
344
|
+
throw new ContextNotFoundError(`Context not found${suffix}`);
|
|
345
|
+
}
|
|
346
|
+
if (response.status === 409 && errorCode === "context_locked") {
|
|
347
|
+
throw new ContextLockedError(errorMessage, {
|
|
348
|
+
activeSessionId: getString(metadata.activeSessionId),
|
|
349
|
+
retryAfter: typeof metadata.retryAfter === "number" ? metadata.retryAfter : void 0
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (response.status === 409 && errorCode === "session_active") {
|
|
353
|
+
throw new APIError(
|
|
354
|
+
`Cannot ${operation} context: ${errorMessage} (session is active)`,
|
|
355
|
+
{
|
|
356
|
+
statusCode: response.status,
|
|
357
|
+
response: response.data
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
if (response.status === 500) {
|
|
362
|
+
const details = getString(errorData.details);
|
|
363
|
+
throw new APIError(
|
|
364
|
+
`Internal server error (500) during ${operation}: ${errorMessage}${details ? `. Details: ${details}` : ""}`,
|
|
365
|
+
{
|
|
366
|
+
statusCode: response.status,
|
|
367
|
+
response: response.data
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (response.status === 502) {
|
|
372
|
+
throw new APIError(
|
|
373
|
+
`Server gateway error (502) during ${operation}: The service may be temporarily unavailable. Please retry.`,
|
|
374
|
+
{
|
|
375
|
+
statusCode: response.status,
|
|
376
|
+
response: response.data
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (response.status === 503) {
|
|
381
|
+
throw new APIError(
|
|
382
|
+
`Service unavailable (503) during ${operation}: The service is temporarily unavailable. Please retry later.`,
|
|
383
|
+
{
|
|
384
|
+
statusCode: response.status,
|
|
385
|
+
response: response.data
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
if (response.status === 504) {
|
|
390
|
+
throw new APIError(
|
|
391
|
+
`Gateway timeout (504) during ${operation}: The request timed out. Please retry.`,
|
|
392
|
+
{
|
|
393
|
+
statusCode: response.status,
|
|
394
|
+
response: response.data
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
throw new APIError(`Failed to ${operation} context: ${errorMessage}`, {
|
|
399
|
+
statusCode: response.status,
|
|
400
|
+
response: response.data
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// src/sessions.ts
|
|
406
|
+
function asRecord2(value) {
|
|
407
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
408
|
+
}
|
|
409
|
+
function getString2(value) {
|
|
410
|
+
return typeof value === "string" ? value : void 0;
|
|
411
|
+
}
|
|
412
|
+
function getNumber(value) {
|
|
413
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
414
|
+
}
|
|
415
|
+
function normalizeContextMode(mode) {
|
|
416
|
+
if (mode === "readWrite" || mode === "read_write") {
|
|
417
|
+
return "read_write";
|
|
418
|
+
}
|
|
419
|
+
return "read_only";
|
|
420
|
+
}
|
|
421
|
+
var PaginationInfo = class {
|
|
422
|
+
constructor(shape) {
|
|
423
|
+
this.currentPage = shape.currentPage;
|
|
424
|
+
this.pageSize = shape.pageSize;
|
|
425
|
+
this.totalCount = shape.totalCount;
|
|
426
|
+
this.totalPages = shape.totalPages;
|
|
427
|
+
this.activeCount = shape.activeCount;
|
|
428
|
+
this.closedCount = shape.closedCount;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
var SessionInfo = class {
|
|
432
|
+
/** @internal */
|
|
433
|
+
constructor(options) {
|
|
434
|
+
this.closed = false;
|
|
435
|
+
this.id = options.id;
|
|
436
|
+
this.sessionId = options.id;
|
|
437
|
+
this.status = options.status;
|
|
438
|
+
this.apiKey = options.apiKey;
|
|
439
|
+
this.projectId = options.projectId;
|
|
440
|
+
this.browserType = options.browserType;
|
|
441
|
+
this.createdAt = options.createdAt;
|
|
442
|
+
this.inspectUrl = options.inspectUrl;
|
|
443
|
+
this.containerId = options.containerId;
|
|
444
|
+
this.inspectUrlDbg = options.inspectUrlDbg;
|
|
445
|
+
this.ws = options.ws ?? null;
|
|
446
|
+
this.client = options.client;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Playwright/CDP connection URL.
|
|
450
|
+
*/
|
|
451
|
+
get connectUrl() {
|
|
452
|
+
return this.ws ?? "";
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Parsed session creation time.
|
|
456
|
+
*/
|
|
457
|
+
get createdAtDate() {
|
|
458
|
+
const date = new Date(this.createdAt);
|
|
459
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Close the current session.
|
|
463
|
+
*
|
|
464
|
+
* This method is idempotent. Errors are logged and swallowed to keep cleanup safe.
|
|
465
|
+
*/
|
|
466
|
+
async close() {
|
|
467
|
+
if (this.closed) {
|
|
468
|
+
getLogger().debug(`Session ${this.sessionId} is already closed.`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!this.client) {
|
|
472
|
+
getLogger().warn(
|
|
473
|
+
`Cannot close session ${this.sessionId}: client was not provided during session creation.`
|
|
474
|
+
);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
getLogger().debug(`Closing session ${this.sessionId}`);
|
|
479
|
+
await this.client.sessions.delete({
|
|
480
|
+
sessionId: this.sessionId,
|
|
481
|
+
projectId: this.projectId
|
|
482
|
+
});
|
|
483
|
+
this.closed = true;
|
|
484
|
+
getLogger().info(`Session closed: ${this.sessionId}`);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
getLogger().warn(`Failed to close session ${this.sessionId}:`, error);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
var SessionListResponse = class {
|
|
491
|
+
constructor(sessions, pagination) {
|
|
492
|
+
this.sessions = sessions;
|
|
493
|
+
this.pagination = pagination;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Number of sessions in the current page.
|
|
497
|
+
*/
|
|
498
|
+
get length() {
|
|
499
|
+
return this.sessions.length;
|
|
500
|
+
}
|
|
501
|
+
[Symbol.iterator]() {
|
|
502
|
+
return this.sessions[Symbol.iterator]();
|
|
503
|
+
}
|
|
504
|
+
at(index) {
|
|
505
|
+
return this.sessions[index];
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
var SessionsResource = class {
|
|
509
|
+
constructor(client) {
|
|
510
|
+
this.client = client;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Create a new browser session.
|
|
514
|
+
*/
|
|
515
|
+
async create(options = {}) {
|
|
516
|
+
const url = `${this.client.baseUrl}/instance`;
|
|
517
|
+
const payload = {
|
|
518
|
+
api_key: this.client.apiKey,
|
|
519
|
+
project_id: options.projectId ?? this.client.projectId,
|
|
520
|
+
browser_mode: options.browserMode ?? "normal"
|
|
521
|
+
};
|
|
522
|
+
if (options.context) {
|
|
523
|
+
const contextPayload = {
|
|
524
|
+
id: options.context.id,
|
|
525
|
+
mode: normalizeContextMode(options.context.mode)
|
|
526
|
+
};
|
|
527
|
+
payload.context = contextPayload;
|
|
528
|
+
getLogger().debug(
|
|
529
|
+
`Creating session with context (id=${options.context.id}, mode=${contextPayload.mode})`
|
|
530
|
+
);
|
|
531
|
+
} else {
|
|
532
|
+
getLogger().debug(`Creating session with browser_mode=${payload.browser_mode}`);
|
|
533
|
+
}
|
|
534
|
+
const response = await this.client._post(url, payload);
|
|
535
|
+
if (response.status >= 400) {
|
|
536
|
+
this.handleCreateError(response);
|
|
537
|
+
}
|
|
538
|
+
const result = asRecord2(response.data);
|
|
539
|
+
const sessionId = getString2(result.session_id);
|
|
540
|
+
if (!sessionId) {
|
|
541
|
+
throw new APIError("Failed to create session: session_id missing from response", {
|
|
542
|
+
statusCode: response.status,
|
|
543
|
+
response: response.data
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
const containerId = getString2(result.container_id) ?? null;
|
|
547
|
+
const wsUrl = await this._getWebSocketDebuggerUrl(sessionId);
|
|
548
|
+
const projectId = options.projectId ?? this.client.projectId;
|
|
549
|
+
getLogger().info(`Session created successfully: id=${sessionId}, container_id=${containerId}`);
|
|
550
|
+
return new SessionInfo({
|
|
551
|
+
id: sessionId,
|
|
552
|
+
status: "active",
|
|
553
|
+
apiKey: this.client.apiKey,
|
|
554
|
+
projectId,
|
|
555
|
+
browserType: options.browserMode ?? "normal",
|
|
556
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
557
|
+
inspectUrl: `${this.client.baseUrl}/inspect?session_id=${sessionId}`,
|
|
558
|
+
containerId,
|
|
559
|
+
ws: wsUrl,
|
|
560
|
+
client: this.client
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* List sessions for the current project.
|
|
565
|
+
*/
|
|
566
|
+
async list(options = {}) {
|
|
567
|
+
const url = `${this.client.baseUrl}/instance/v2/sessions`;
|
|
568
|
+
const payload = {
|
|
569
|
+
api_key: this.client.apiKey,
|
|
570
|
+
project_id: options.projectId ?? this.client.projectId
|
|
571
|
+
};
|
|
572
|
+
if (options.status) {
|
|
573
|
+
payload.status = options.status;
|
|
574
|
+
}
|
|
575
|
+
const response = await this.client._post(url, payload);
|
|
576
|
+
if (response.status >= 400) {
|
|
577
|
+
this.handleListError(response);
|
|
578
|
+
}
|
|
579
|
+
const result = asRecord2(response.data);
|
|
580
|
+
const sessionsData = Array.isArray(result.sessions) ? result.sessions : [];
|
|
581
|
+
const paginationData = asRecord2(result.pagination);
|
|
582
|
+
const sessions = sessionsData.map((item) => {
|
|
583
|
+
const session = asRecord2(item);
|
|
584
|
+
const sessionId = getString2(session.id) ?? "";
|
|
585
|
+
return new SessionInfo({
|
|
586
|
+
id: sessionId,
|
|
587
|
+
status: getString2(session.status) ?? "active",
|
|
588
|
+
apiKey: getString2(session.api_key) ?? this.client.apiKey,
|
|
589
|
+
projectId: getString2(session.project_id) ?? (options.projectId ?? this.client.projectId),
|
|
590
|
+
browserType: getString2(session.browser_type) ?? "normal",
|
|
591
|
+
createdAt: getString2(session.created_at) ?? "",
|
|
592
|
+
inspectUrl: getString2(session.inspect_url) ?? `${this.client.baseUrl}/inspect?session_id=${sessionId}`,
|
|
593
|
+
containerId: getString2(session.container_id) ?? null,
|
|
594
|
+
inspectUrlDbg: getString2(session.inspect_url_dbg),
|
|
595
|
+
ws: getString2(session.ws) ?? null,
|
|
596
|
+
client: this.client
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
const pagination = new PaginationInfo({
|
|
600
|
+
currentPage: getNumber(paginationData.currentPage) ?? 1,
|
|
601
|
+
pageSize: getNumber(paginationData.pageSize) ?? sessions.length,
|
|
602
|
+
totalCount: getNumber(paginationData.totalCount) ?? sessions.length,
|
|
603
|
+
totalPages: getNumber(paginationData.totalPages) ?? 1,
|
|
604
|
+
activeCount: getNumber(paginationData.activeCount) ?? 0,
|
|
605
|
+
closedCount: getNumber(paginationData.closedCount) ?? 0
|
|
606
|
+
});
|
|
607
|
+
getLogger().info(
|
|
608
|
+
`Retrieved ${sessions.length} sessions (total: ${pagination.totalCount}, active: ${pagination.activeCount}, closed: ${pagination.closedCount})`
|
|
609
|
+
);
|
|
610
|
+
return new SessionListResponse(sessions, pagination);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Delete a browser session.
|
|
614
|
+
*/
|
|
615
|
+
async delete(options) {
|
|
616
|
+
const url = `${this.client.baseUrl}/instance`;
|
|
617
|
+
const payload = {
|
|
618
|
+
api_key: this.client.apiKey,
|
|
619
|
+
project_id: options.projectId ?? this.client.projectId,
|
|
620
|
+
session_id: options.sessionId
|
|
621
|
+
};
|
|
622
|
+
const response = await this.client._delete(url, payload);
|
|
623
|
+
if (response.status >= 400) {
|
|
624
|
+
this.handleDeleteError(response, options.sessionId);
|
|
625
|
+
}
|
|
626
|
+
getLogger().info(`Session deleted successfully: ${options.sessionId}`);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Fetch the debugger WebSocket URL for a session.
|
|
630
|
+
*
|
|
631
|
+
* @internal
|
|
632
|
+
*/
|
|
633
|
+
async _getWebSocketDebuggerUrl(sessionId) {
|
|
634
|
+
try {
|
|
635
|
+
const response = await this.client._get(`${this.client.baseUrl}/json/version`, {
|
|
636
|
+
session_id: sessionId
|
|
637
|
+
});
|
|
638
|
+
if (response.status >= 400) {
|
|
639
|
+
getLogger().error("Error getting WebSocket debugger URL:", response.data);
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
const result = asRecord2(response.data);
|
|
643
|
+
return getString2(result.webSocketDebuggerUrlTransformed) ?? getString2(result.webSocketDebuggerUrl) ?? null;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
getLogger().error("Error getting WebSocket debugger URL:", error);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
handleCreateError(response) {
|
|
650
|
+
const errorData = asRecord2(response.data);
|
|
651
|
+
const errorMessage = getString2(errorData.error) ?? getString2(errorData.message) ?? "Unknown error";
|
|
652
|
+
const errorCode = getString2(errorData.code);
|
|
653
|
+
const metadata = asRecord2(errorData.metadata);
|
|
654
|
+
if (response.status === 401) {
|
|
655
|
+
throw new AuthenticationError(
|
|
656
|
+
`Authentication failed: ${errorMessage}. Please check your API key and project ID.`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (response.status === 404 && errorCode === "context_not_found") {
|
|
660
|
+
throw new ContextNotFoundError(`Context not found: ${errorMessage}`);
|
|
661
|
+
}
|
|
662
|
+
if (response.status === 404) {
|
|
663
|
+
throw new SessionNotFoundError(`Resource not found: ${errorMessage}`);
|
|
664
|
+
}
|
|
665
|
+
if (response.status === 409 && errorCode === "context_locked") {
|
|
666
|
+
throw new ContextLockedError(errorMessage, {
|
|
667
|
+
activeSessionId: getString2(metadata.activeSessionId),
|
|
668
|
+
retryAfter: getNumber(metadata.retryAfter)
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
throw new APIError(`Failed to create session: ${errorMessage}`, {
|
|
672
|
+
statusCode: response.status,
|
|
673
|
+
response: response.data
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
handleListError(response) {
|
|
677
|
+
const errorData = asRecord2(response.data);
|
|
678
|
+
const errorMessage = getString2(errorData.error) ?? getString2(errorData.message) ?? "Unknown error";
|
|
679
|
+
if (response.status === 401) {
|
|
680
|
+
throw new AuthenticationError(
|
|
681
|
+
`Authentication failed: ${errorMessage}. Please check your API key and project ID.`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
throw new APIError(`Failed to list sessions: ${errorMessage}`, {
|
|
685
|
+
statusCode: response.status,
|
|
686
|
+
response: response.data
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
handleDeleteError(response, sessionId) {
|
|
690
|
+
const errorData = asRecord2(response.data);
|
|
691
|
+
const errorMessage = getString2(errorData.error) ?? getString2(errorData.message) ?? "Unknown error";
|
|
692
|
+
if (response.status === 401) {
|
|
693
|
+
throw new AuthenticationError(
|
|
694
|
+
`Authentication failed: ${errorMessage}. Please check your API key and project ID.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (response.status === 404) {
|
|
698
|
+
throw new SessionNotFoundError(
|
|
699
|
+
`Session not found: ${errorMessage}. Session ID '${sessionId}' may have already been deleted or never existed.`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
throw new APIError(`Failed to delete session: ${errorMessage}`, {
|
|
703
|
+
statusCode: response.status,
|
|
704
|
+
response: response.data
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// src/client.ts
|
|
710
|
+
dotenv.config();
|
|
711
|
+
var Lexmount = class {
|
|
712
|
+
constructor(config2 = {}) {
|
|
713
|
+
this.apiKey = config2.apiKey ?? process.env.LEXMOUNT_API_KEY ?? "";
|
|
714
|
+
if (!this.apiKey) {
|
|
715
|
+
throw new ValidationError(
|
|
716
|
+
"apiKey must be provided or set LEXMOUNT_API_KEY environment variable"
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
this.projectId = config2.projectId ?? process.env.LEXMOUNT_PROJECT_ID ?? "";
|
|
720
|
+
if (!this.projectId) {
|
|
721
|
+
throw new ValidationError(
|
|
722
|
+
"projectId must be provided or set LEXMOUNT_PROJECT_ID environment variable"
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
this.baseUrl = (config2.baseUrl ?? process.env.LEXMOUNT_BASE_URL ?? "https://api.lexmount.cn").replace(/\/$/, "");
|
|
726
|
+
if (config2.logLevel) {
|
|
727
|
+
setLogLevel(config2.logLevel);
|
|
728
|
+
}
|
|
729
|
+
this.httpClient = axios.create({
|
|
730
|
+
timeout: config2.timeout ?? 6e4,
|
|
731
|
+
validateStatus: () => true,
|
|
732
|
+
headers: {
|
|
733
|
+
"Content-Type": "application/json"
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
this.sessions = new SessionsResource(this);
|
|
737
|
+
this.contexts = new ContextsResource(this);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Internal POST request helper with timeout and network error mapping.
|
|
741
|
+
*
|
|
742
|
+
* @internal
|
|
743
|
+
*/
|
|
744
|
+
async _post(url, data) {
|
|
745
|
+
return this.request("POST", url, { data });
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Internal GET request helper with timeout and network error mapping.
|
|
749
|
+
*
|
|
750
|
+
* @internal
|
|
751
|
+
*/
|
|
752
|
+
async _get(url, params) {
|
|
753
|
+
return this.request("GET", url, { params });
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Internal DELETE request helper with timeout and network error mapping.
|
|
757
|
+
*
|
|
758
|
+
* @internal
|
|
759
|
+
*/
|
|
760
|
+
async _delete(url, data) {
|
|
761
|
+
return this.request("DELETE", url, { data });
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Close the client.
|
|
765
|
+
*
|
|
766
|
+
* Axios does not require explicit teardown, but the method is kept for API symmetry.
|
|
767
|
+
*/
|
|
768
|
+
close() {
|
|
769
|
+
getLogger().debug("Closing Lexmount client");
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* String representation of the client.
|
|
773
|
+
*/
|
|
774
|
+
toString() {
|
|
775
|
+
return `Lexmount(projectId='${this.projectId}', baseUrl='${this.baseUrl}')`;
|
|
776
|
+
}
|
|
777
|
+
async request(method, url, config2) {
|
|
778
|
+
const logger2 = getLogger();
|
|
779
|
+
const startTime = Date.now();
|
|
780
|
+
logger2.debug(`${method} request to ${url}`);
|
|
781
|
+
try {
|
|
782
|
+
const response = await this.httpClient.request({
|
|
783
|
+
method,
|
|
784
|
+
url,
|
|
785
|
+
...config2
|
|
786
|
+
});
|
|
787
|
+
const elapsed = Date.now() - startTime;
|
|
788
|
+
logger2.debug(`${method} response: status=${response.status}, duration=${elapsed.toFixed(2)}ms`);
|
|
789
|
+
return response;
|
|
790
|
+
} catch (error) {
|
|
791
|
+
const elapsed = Date.now() - startTime;
|
|
792
|
+
throw this.normalizeRequestError(error, elapsed);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
normalizeRequestError(error, elapsedMs) {
|
|
796
|
+
const logger2 = getLogger();
|
|
797
|
+
if (!isAxiosError(error)) {
|
|
798
|
+
logger2.error(`Unexpected request error after ${elapsedMs.toFixed(2)}ms:`, error);
|
|
799
|
+
return new APIError("Unexpected HTTP error", { response: error });
|
|
800
|
+
}
|
|
801
|
+
const axiosError = error;
|
|
802
|
+
if (axiosError.code === "ECONNABORTED") {
|
|
803
|
+
logger2.error(`Request timeout after ${elapsedMs.toFixed(2)}ms:`, axiosError.message);
|
|
804
|
+
return new TimeoutError(`Request timed out: ${axiosError.message}`);
|
|
805
|
+
}
|
|
806
|
+
if (axiosError.response) {
|
|
807
|
+
logger2.error(`HTTP error after ${elapsedMs.toFixed(2)}ms:`, axiosError.message);
|
|
808
|
+
return new APIError(`HTTP error: ${axiosError.message}`, {
|
|
809
|
+
statusCode: axiosError.response.status,
|
|
810
|
+
response: axiosError.response.data
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
logger2.error(`Network error after ${elapsedMs.toFixed(2)}ms:`, axiosError.message);
|
|
814
|
+
return new NetworkError(`Network error: ${axiosError.message}`);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/index.ts
|
|
819
|
+
var VERSION = "0.2.0";
|
|
820
|
+
export {
|
|
821
|
+
APIError,
|
|
822
|
+
AuthenticationError,
|
|
823
|
+
ContextInfo,
|
|
824
|
+
ContextListResponse,
|
|
825
|
+
ContextLockedError,
|
|
826
|
+
ContextNotFoundError,
|
|
827
|
+
ContextsResource,
|
|
828
|
+
Lexmount,
|
|
829
|
+
LexmountError,
|
|
830
|
+
LexmountLogger,
|
|
831
|
+
NetworkError,
|
|
832
|
+
PaginationInfo,
|
|
833
|
+
SessionInfo,
|
|
834
|
+
SessionListResponse,
|
|
835
|
+
SessionNotFoundError,
|
|
836
|
+
SessionsResource,
|
|
837
|
+
TimeoutError,
|
|
838
|
+
VERSION,
|
|
839
|
+
ValidationError,
|
|
840
|
+
Lexmount as default,
|
|
841
|
+
disableLogging,
|
|
842
|
+
enableLogging,
|
|
843
|
+
getLogger,
|
|
844
|
+
setLogLevel
|
|
845
|
+
};
|
|
846
|
+
//# sourceMappingURL=index.mjs.map
|