@ai-digital-factory/lmt-web-cli 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2686 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { spawn as spawn2 } from "node:child_process";
5
+ import { createRequire } from "node:module";
6
+ import path5 from "node:path";
7
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
8
+
9
+ // ../cli/src/profile-store.ts
10
+ import { randomUUID } from "node:crypto";
11
+ import {
12
+ chmod,
13
+ constants,
14
+ lstat,
15
+ mkdir,
16
+ open,
17
+ rename,
18
+ unlink
19
+ } from "node:fs/promises";
20
+ import path from "node:path";
21
+
22
+ // ../cli/src/profile-name.ts
23
+ var CLIENT_PROFILE_NAME_ERROR = "Profile must be 1\u201364 letters, digits, dots, underscores, or hyphens, starting with a letter or digit.";
24
+ var clientProfilePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
25
+ function assertClientProfileName(profile) {
26
+ if (!clientProfilePattern.test(profile)) throw new Error(CLIENT_PROFILE_NAME_ERROR);
27
+ }
28
+
29
+ // ../cli/src/profile-store.ts
30
+ var FILE_NAME = "secrets.pem";
31
+ var OWNER_ONLY_DIRECTORY = 448;
32
+ var OWNER_ONLY_FILE = 384;
33
+ var LOCK_WAIT_MS = 5e3;
34
+ var LOCK_RETRY_MS = 20;
35
+ function emptyRecord() {
36
+ return { version: 2, profiles: {} };
37
+ }
38
+ function targetsEqual(left, right) {
39
+ return left.environment === right.environment && left.origin === right.origin && left.operatorId === right.operatorId;
40
+ }
41
+ function assertCookie(value, name) {
42
+ if (value !== void 0 && !new RegExp(`^${name}=[A-Za-z0-9._~-]+$`).test(value)) {
43
+ throw new Error(`The ${name} cookie is not valid.`);
44
+ }
45
+ }
46
+ function ownerId() {
47
+ return typeof process.getuid === "function" ? process.getuid() : void 0;
48
+ }
49
+ async function ensureOwnerOnly(pathname, kind) {
50
+ let info;
51
+ try {
52
+ info = await lstat(pathname);
53
+ } catch (error) {
54
+ if (error.code === "ENOENT") throw error;
55
+ throw new Error("Could not inspect the protected CLI profile storage.");
56
+ }
57
+ if (info.isSymbolicLink() || (kind === "directory" ? !info.isDirectory() : !info.isFile())) {
58
+ throw new Error(`The protected CLI profile ${kind} must be a regular ${kind}.`);
59
+ }
60
+ const uid = ownerId();
61
+ if (uid !== void 0 && info.uid !== uid) {
62
+ throw new Error(`The protected CLI profile ${kind} must be owned by the current user.`);
63
+ }
64
+ const expectedMode = kind === "directory" ? OWNER_ONLY_DIRECTORY : OWNER_ONLY_FILE;
65
+ if ((info.mode & 511) === expectedMode) return;
66
+ try {
67
+ await chmod(pathname, expectedMode);
68
+ } catch {
69
+ throw new Error(`Could not set protected CLI profile ${kind} permissions.`);
70
+ }
71
+ const updated = await lstat(pathname);
72
+ if ((updated.mode & 511) !== expectedMode) {
73
+ const mode = expectedMode.toString(8).padStart(4, "0");
74
+ throw new Error(`The protected CLI profile ${kind} must have mode ${mode}.`);
75
+ }
76
+ }
77
+ function parseLegacyTargetKey(key) {
78
+ try {
79
+ const parsed = JSON.parse(key);
80
+ if (!Array.isArray(parsed) || parsed.length !== 3) return void 0;
81
+ const [environment, origin, operatorId] = parsed;
82
+ if (environment !== "local" && environment !== "dev") return void 0;
83
+ if (typeof origin !== "string" || !origin.trim()) return void 0;
84
+ if (operatorId !== "OPERATOR_1") return void 0;
85
+ return { environment, origin: origin.trim(), operatorId };
86
+ } catch {
87
+ return void 0;
88
+ }
89
+ }
90
+ function migrateLegacyRecord(record) {
91
+ const migrated = emptyRecord();
92
+ if (record.smsCredential?.trim()) migrated.smsCredential = record.smsCredential;
93
+ for (const [profileName, profileRecord] of Object.entries(record.profiles ?? {})) {
94
+ const sessions = [];
95
+ for (const [targetKey, stored] of Object.entries(profileRecord.targets ?? {})) {
96
+ const target = parseLegacyTargetKey(targetKey);
97
+ if (!target) continue;
98
+ sessions.push({
99
+ ...target,
100
+ ...stored.operatorSessionCookie ? { operatorSessionCookie: stored.operatorSessionCookie } : {},
101
+ ...stored.clientIdentityCookie ? { clientIdentityCookie: stored.clientIdentityCookie } : {},
102
+ updatedAt: stored.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
103
+ });
104
+ }
105
+ if (sessions.length > 0) migrated.profiles[profileName] = { sessions };
106
+ }
107
+ return migrated;
108
+ }
109
+ function normalizeSession(value) {
110
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
111
+ const session = value;
112
+ if (session.environment !== "local" && session.environment !== "dev") return void 0;
113
+ if (typeof session.origin !== "string" || !session.origin.trim()) return void 0;
114
+ if (session.operatorId !== "OPERATOR_1") return void 0;
115
+ if (typeof session.updatedAt !== "string" || !session.updatedAt.trim()) return void 0;
116
+ if (session.operatorSessionCookie !== void 0 && typeof session.operatorSessionCookie !== "string") return void 0;
117
+ if (session.clientIdentityCookie !== void 0 && typeof session.clientIdentityCookie !== "string") return void 0;
118
+ return {
119
+ environment: session.environment,
120
+ origin: session.origin.trim(),
121
+ operatorId: session.operatorId,
122
+ updatedAt: session.updatedAt,
123
+ ...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
124
+ ...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {}
125
+ };
126
+ }
127
+ function parseRecord(raw) {
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(raw);
131
+ } catch {
132
+ throw new Error("The protected CLI profile file is not valid JSON.");
133
+ }
134
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
135
+ throw new Error("The protected CLI profile file has an unsupported format.");
136
+ }
137
+ const record = parsed;
138
+ if (record.version === 1) {
139
+ return migrateLegacyRecord(parsed);
140
+ }
141
+ if (record.version !== 2 || !record.profiles || typeof record.profiles !== "object" || Array.isArray(record.profiles)) {
142
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
143
+ }
144
+ if (record.smsCredential !== void 0 && (typeof record.smsCredential !== "string" || !record.smsCredential.trim())) {
145
+ throw new Error("The protected CLI profile file has an invalid SMS credential record.");
146
+ }
147
+ const profiles = {};
148
+ for (const [profileName, profileValue] of Object.entries(record.profiles)) {
149
+ if (!profileValue || typeof profileValue !== "object" || Array.isArray(profileValue)) {
150
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
151
+ }
152
+ const sessionsValue = profileValue.sessions;
153
+ if (!Array.isArray(sessionsValue)) {
154
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
155
+ }
156
+ const sessions = sessionsValue.map(normalizeSession).filter((session) => session !== void 0);
157
+ if (sessions.length !== sessionsValue.length) {
158
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
159
+ }
160
+ if (sessions.length > 0) profiles[profileName] = { sessions };
161
+ }
162
+ return {
163
+ version: 2,
164
+ ...typeof record.smsCredential === "string" && record.smsCredential.trim() ? { smsCredential: record.smsCredential } : {},
165
+ profiles
166
+ };
167
+ }
168
+ function findSessionIndex(sessions, target) {
169
+ return sessions.findIndex((session) => targetsEqual(session, target));
170
+ }
171
+ function sessionHasCookies(session) {
172
+ return Boolean(session.operatorSessionCookie || session.clientIdentityCookie);
173
+ }
174
+ var CliProfileStore = class {
175
+ constructor(homeDirectory = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd(), lockWaitMs = LOCK_WAIT_MS, directoryName = ".lmt") {
176
+ this.lockWaitMs = lockWaitMs;
177
+ this.directory = path.join(homeDirectory, directoryName);
178
+ this.filePath = path.join(this.directory, FILE_NAME);
179
+ this.lockPath = path.join(this.directory, `${FILE_NAME}.lock`);
180
+ }
181
+ lockWaitMs;
182
+ directory;
183
+ filePath;
184
+ lockPath;
185
+ async getSmsCredential() {
186
+ return this.withLock(async () => (await this.readRecord()).smsCredential);
187
+ }
188
+ async saveSmsCredential(credential) {
189
+ if (!credential.trim() || /[\r\n]/.test(credential)) throw new Error("SMS credential input must be a non-empty single line.");
190
+ await this.withLock(async () => {
191
+ const record = await this.readRecordForSmsCredentialUpdate();
192
+ record.smsCredential = credential;
193
+ await this.writeRecord(record);
194
+ });
195
+ }
196
+ async getSession(profile, target) {
197
+ assertClientProfileName(profile);
198
+ return this.withLock(async () => {
199
+ const stored = (await this.readRecord()).profiles[profile]?.sessions.find((session) => targetsEqual(session, target));
200
+ if (!stored) return void 0;
201
+ return {
202
+ ...stored.operatorSessionCookie ? { operatorSessionCookie: stored.operatorSessionCookie } : {},
203
+ ...stored.clientIdentityCookie ? { clientIdentityCookie: stored.clientIdentityCookie } : {}
204
+ };
205
+ });
206
+ }
207
+ async saveSession(profile, target, cookies) {
208
+ assertClientProfileName(profile);
209
+ assertCookie(cookies.operatorSessionCookie, "lmt_operator_session");
210
+ assertCookie(cookies.clientIdentityCookie, "lmt_client_id");
211
+ if (!cookies.operatorSessionCookie && !cookies.clientIdentityCookie) return;
212
+ await this.update((record) => {
213
+ const profileRecord = record.profiles[profile] ?? { sessions: [] };
214
+ const index = findSessionIndex(profileRecord.sessions, target);
215
+ const next = {
216
+ ...target,
217
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
218
+ ...cookies.clientIdentityCookie ? { clientIdentityCookie: cookies.clientIdentityCookie } : {},
219
+ ...cookies.operatorSessionCookie ? { operatorSessionCookie: cookies.operatorSessionCookie } : {}
220
+ };
221
+ if (!sessionHasCookies(next)) {
222
+ if (index >= 0) profileRecord.sessions.splice(index, 1);
223
+ } else if (index >= 0) {
224
+ profileRecord.sessions[index] = next;
225
+ } else {
226
+ profileRecord.sessions.push(next);
227
+ }
228
+ if (profileRecord.sessions.length === 0) delete record.profiles[profile];
229
+ else record.profiles[profile] = profileRecord;
230
+ });
231
+ }
232
+ async clearOperatorSession(profile, target) {
233
+ assertClientProfileName(profile);
234
+ await this.update((record) => {
235
+ const profileRecord = record.profiles[profile];
236
+ if (!profileRecord) return;
237
+ const index = findSessionIndex(profileRecord.sessions, target);
238
+ if (index < 0) return;
239
+ const stored = profileRecord.sessions[index];
240
+ delete stored.operatorSessionCookie;
241
+ stored.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
242
+ this.cleanEmptySession(profileRecord, index);
243
+ if (profileRecord.sessions.length === 0) delete record.profiles[profile];
244
+ });
245
+ }
246
+ async removeTarget(profile, target) {
247
+ assertClientProfileName(profile);
248
+ await this.update((record) => {
249
+ const profileRecord = record.profiles[profile];
250
+ if (!profileRecord) return;
251
+ profileRecord.sessions = profileRecord.sessions.filter((session) => !targetsEqual(session, target));
252
+ if (profileRecord.sessions.length === 0) delete record.profiles[profile];
253
+ });
254
+ }
255
+ cleanEmptySession(profileRecord, index) {
256
+ const stored = profileRecord.sessions[index];
257
+ if (stored && !sessionHasCookies(stored)) profileRecord.sessions.splice(index, 1);
258
+ }
259
+ async update(mutator) {
260
+ await this.withLock(async () => {
261
+ const record = await this.readRecord();
262
+ mutator(record);
263
+ await this.writeRecord(record);
264
+ });
265
+ }
266
+ async withLock(operation) {
267
+ await this.ensureDirectory();
268
+ const started = Date.now();
269
+ let lock;
270
+ while (!lock) {
271
+ try {
272
+ lock = await open(this.lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), OWNER_ONLY_FILE);
273
+ await lock.chmod(OWNER_ONLY_FILE);
274
+ await lock.writeFile(`${process.pid}
275
+ `);
276
+ await lock.sync();
277
+ } catch (error) {
278
+ if (error.code !== "EEXIST") {
279
+ throw new Error("Could not acquire the protected CLI profile storage lock.");
280
+ }
281
+ try {
282
+ await ensureOwnerOnly(this.lockPath, "file");
283
+ } catch (lockError) {
284
+ if (lockError.code === "ENOENT") continue;
285
+ throw lockError;
286
+ }
287
+ if (Date.now() - started >= this.lockWaitMs) {
288
+ throw new Error("Timed out waiting for the protected CLI profile storage lock.");
289
+ }
290
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
291
+ }
292
+ }
293
+ try {
294
+ return await operation();
295
+ } finally {
296
+ await lock.close().catch(() => void 0);
297
+ await unlink(this.lockPath).catch(() => void 0);
298
+ }
299
+ }
300
+ async ensureDirectory() {
301
+ let created = false;
302
+ try {
303
+ await mkdir(this.directory, { mode: OWNER_ONLY_DIRECTORY });
304
+ created = true;
305
+ } catch (error) {
306
+ if (error.code !== "EEXIST") {
307
+ throw new Error("Could not create the protected CLI profile directory.");
308
+ }
309
+ }
310
+ if (created) {
311
+ try {
312
+ await chmod(this.directory, OWNER_ONLY_DIRECTORY);
313
+ } catch {
314
+ throw new Error("Could not set protected CLI profile directory permissions.");
315
+ }
316
+ }
317
+ await ensureOwnerOnly(this.directory, "directory");
318
+ }
319
+ isRecoverableStoreFormatError(error) {
320
+ if (!(error instanceof Error)) return false;
321
+ return error.message === "The protected CLI profile file is not valid JSON." || error.message === "The protected CLI profile file has an unsupported format." || error.message === "The protected CLI profile file has an unsupported version or format." || error.message === "The protected CLI profile file has an invalid SMS credential record.";
322
+ }
323
+ async archiveUnreadableStoreFile() {
324
+ const backupPath = `${this.filePath}.corrupt.${Date.now()}.bak`;
325
+ try {
326
+ await rename(this.filePath, backupPath);
327
+ } catch {
328
+ throw new Error("Could not repair the protected CLI profile file.");
329
+ }
330
+ }
331
+ async readRecordForSmsCredentialUpdate() {
332
+ try {
333
+ return await this.readRecord();
334
+ } catch (error) {
335
+ if (!this.isRecoverableStoreFormatError(error)) throw error;
336
+ await this.archiveUnreadableStoreFile();
337
+ return emptyRecord();
338
+ }
339
+ }
340
+ async readRecord() {
341
+ try {
342
+ const info = await lstat(this.filePath);
343
+ if (info.isSymbolicLink() || !info.isFile()) {
344
+ throw new Error("The protected CLI profile file must be a regular file.");
345
+ }
346
+ await ensureOwnerOnly(this.filePath, "file");
347
+ const handle = await open(this.filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
348
+ try {
349
+ return parseRecord(await handle.readFile("utf8"));
350
+ } finally {
351
+ await handle.close();
352
+ }
353
+ } catch (error) {
354
+ if (error.code === "ENOENT") return emptyRecord();
355
+ if (error instanceof Error && error.message.startsWith("The protected CLI profile")) throw error;
356
+ throw new Error("Could not read the protected CLI profile file.");
357
+ }
358
+ }
359
+ async writeRecord(record) {
360
+ const normalized = {
361
+ version: 2,
362
+ ...record.smsCredential ? { smsCredential: record.smsCredential } : {},
363
+ profiles: Object.fromEntries(
364
+ Object.entries(record.profiles).map(([profileName, profileRecord]) => [
365
+ profileName,
366
+ {
367
+ sessions: profileRecord.sessions.map((session) => ({
368
+ environment: session.environment,
369
+ origin: session.origin,
370
+ operatorId: session.operatorId,
371
+ ...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
372
+ ...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {},
373
+ updatedAt: session.updatedAt
374
+ }))
375
+ }
376
+ ])
377
+ )
378
+ };
379
+ const temporaryPath = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
380
+ const content = `${JSON.stringify(normalized, null, 2)}
381
+ `;
382
+ let handle;
383
+ try {
384
+ handle = await open(temporaryPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), OWNER_ONLY_FILE);
385
+ await handle.chmod(OWNER_ONLY_FILE);
386
+ await handle.writeFile(content, "utf8");
387
+ await handle.sync();
388
+ await handle.close();
389
+ handle = void 0;
390
+ await rename(temporaryPath, this.filePath);
391
+ const directoryHandle = await open(this.directory, constants.O_RDONLY);
392
+ try {
393
+ await directoryHandle.sync();
394
+ } finally {
395
+ await directoryHandle.close();
396
+ }
397
+ } catch {
398
+ if (handle) await handle.close().catch(() => void 0);
399
+ await unlink(temporaryPath).catch(() => void 0);
400
+ throw new Error("Could not atomically update the protected CLI profile file.");
401
+ }
402
+ }
403
+ };
404
+
405
+ // ../cli/src/config.ts
406
+ import { existsSync, readFileSync } from "node:fs";
407
+ import path2 from "node:path";
408
+ import { fileURLToPath } from "node:url";
409
+ var repoRoot = path2.resolve(
410
+ path2.dirname(fileURLToPath(import.meta.url)),
411
+ "../../../../"
412
+ );
413
+ var packagedConfigPath = fileURLToPath(new URL("../lmt.config.json", import.meta.url));
414
+ var DEFAULT_CONFIG_PATH = existsSync(packagedConfigPath) ? packagedConfigPath : path2.join(repoRoot, "lmt.config.json");
415
+ function resolveCanonicalOrigin(value, environment) {
416
+ if (typeof value !== "string" || !value.trim()) {
417
+ throw new Error(`lmt.config.json does not contain the ${environment} Origin.`);
418
+ }
419
+ let origin;
420
+ try {
421
+ origin = new URL(value.trim());
422
+ } catch {
423
+ throw new Error(`${environment} Origin must be an absolute HTTP URL.`);
424
+ }
425
+ if (!/^https?:$/.test(origin.protocol) || origin.username || origin.password || origin.hash) {
426
+ throw new Error(
427
+ `${environment} Origin must be an HTTP URL without credentials or a fragment.`
428
+ );
429
+ }
430
+ if (origin.pathname !== "/" || origin.search) {
431
+ throw new Error(`${environment} Origin must not include a path or query string.`);
432
+ }
433
+ return origin.origin;
434
+ }
435
+ function resolveCliTarget(env = process.env, configPath = DEFAULT_CONFIG_PATH) {
436
+ const environment = env.LMT_ENV?.trim();
437
+ if (environment !== "local" && environment !== "dev") {
438
+ if (environment === "prod") {
439
+ throw new Error("The CLI supports local or dev targets only; prod is not permitted.");
440
+ }
441
+ throw new Error("Target must explicitly select --env local or --env dev; prod is not permitted.");
442
+ }
443
+ let config;
444
+ try {
445
+ config = JSON.parse(readFileSync(configPath, "utf8"));
446
+ } catch {
447
+ throw new Error("Could not read the deployment configuration lmt.config.json.");
448
+ }
449
+ const selected = config.environments?.[environment];
450
+ if (!selected) {
451
+ throw new Error(`lmt.config.json does not contain the ${environment} environment.`);
452
+ }
453
+ const origin = resolveCanonicalOrigin(selected.origin, environment);
454
+ const smsEndpoint = typeof config.smsEndpoint === "string" ? config.smsEndpoint.trim() : "";
455
+ if (!smsEndpoint) throw new Error("lmt.config.json does not contain smsEndpoint.");
456
+ const gateScope = typeof selected.gateScope === "string" ? selected.gateScope.trim() : "";
457
+ if (!gateScope) throw new Error(`lmt.config.json does not contain the ${environment} Gate scope.`);
458
+ const operators = selected.brokerOperatorIds;
459
+ if (!Array.isArray(operators) || operators.length !== 1 || operators[0] !== "OPERATOR_1") {
460
+ throw new Error(`${environment} must have exactly one configured Operator: OPERATOR_1.`);
461
+ }
462
+ let smsUrl;
463
+ try {
464
+ smsUrl = new URL(smsEndpoint);
465
+ } catch {
466
+ throw new Error("smsEndpoint must be an absolute HTTP URL.");
467
+ }
468
+ if (!/^https?:$/.test(smsUrl.protocol) || smsUrl.username || smsUrl.password || smsUrl.hash) {
469
+ throw new Error("smsEndpoint must be an HTTP URL without credentials or a fragment.");
470
+ }
471
+ return {
472
+ environment,
473
+ origin,
474
+ gateScope,
475
+ smsEndpoint: smsUrl.origin,
476
+ operatorId: "OPERATOR_1"
477
+ };
478
+ }
479
+
480
+ // src/actions.ts
481
+ var InvocationError = class extends Error {
482
+ constructor(message, code = "invalid_invocation", action) {
483
+ super(message);
484
+ this.code = code;
485
+ this.action = action;
486
+ }
487
+ code;
488
+ action;
489
+ };
490
+ function required(input, option) {
491
+ const value = input.values.get(option);
492
+ if (value === void 0 || option !== "--value" && !value) throw new InvocationError(`${option} is required for this action.`);
493
+ return value;
494
+ }
495
+ var ACTIONS = {
496
+ start: { control: false, flags: ["--headed"], values: ["--env", "--device"], build: () => ({ action: "start" }) },
497
+ inspect: { control: false, flags: [], values: [], build: () => ({ action: "inspect" }) },
498
+ close: { control: false, flags: [], values: [], build: () => ({ action: "close" }) },
499
+ stop: { control: false, flags: [], values: [], build: () => ({ action: "stop" }) },
500
+ "operator-login": { control: false, flags: [], values: [], build: () => ({ action: "operator-login" }) },
501
+ "operator-logout": { control: false, flags: [], values: [], build: () => ({ action: "operator-logout" }) },
502
+ "zerodha-login": { control: false, flags: [], values: [], build: () => ({ action: "zerodha-login" }) },
503
+ "zerodha-logout": { control: false, flags: [], values: [], build: () => ({ action: "zerodha-logout" }) },
504
+ "zerodha-otp": {
505
+ control: false,
506
+ flags: ["--stdin"],
507
+ values: ["--attempt"],
508
+ build: async (input) => {
509
+ const attemptId = required(input, "--attempt");
510
+ if (!input.flags.has("--stdin")) throw new InvocationError("zerodha-otp requires --attempt ID and --stdin.");
511
+ return { action: "zerodha-otp", attemptId, otp: await input.readOtp() };
512
+ }
513
+ },
514
+ click: { control: true, flags: [], values: ["--ref"], build: (input) => ({ action: "click", reference: required(input, "--ref") }) },
515
+ fill: {
516
+ control: true,
517
+ flags: ["--secret"],
518
+ values: ["--ref", "--value"],
519
+ build: (input) => ({ action: "fill", reference: required(input, "--ref"), value: required(input, "--value"), secret: input.flags.has("--secret") })
520
+ },
521
+ select: {
522
+ control: true,
523
+ flags: [],
524
+ values: ["--ref", "--value"],
525
+ build: (input) => ({ action: "select", reference: required(input, "--ref"), value: required(input, "--value") })
526
+ },
527
+ scroll: {
528
+ control: true,
529
+ flags: [],
530
+ values: ["--ref", "--x", "--y"],
531
+ build: (input) => ({ action: "scroll", reference: input.values.get("--ref"), x: Number(input.values.get("--x") ?? 0), y: Number(input.values.get("--y") ?? 0) })
532
+ },
533
+ navigate: { control: true, flags: [], values: ["--url"], build: (input) => ({ action: "navigate", url: required(input, "--url") }) }
534
+ };
535
+ function isAction(value) {
536
+ return typeof value === "string" && Object.hasOwn(ACTIONS, value);
537
+ }
538
+ function isControlAction(action) {
539
+ return ACTIONS[action.action].control;
540
+ }
541
+
542
+ // src/config.ts
543
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
544
+ import path3 from "node:path";
545
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
546
+ var repositoryRoot = path3.resolve(
547
+ path3.dirname(fileURLToPath2(import.meta.url)),
548
+ "../../../../"
549
+ );
550
+ var packagedConfig = fileURLToPath2(new URL("../lmt-web.config.json", import.meta.url));
551
+ var DEFAULT_CONFIG_PATH2 = existsSync2(packagedConfig) ? packagedConfig : path3.join(repositoryRoot, "lmt.config.json");
552
+ function canonicalOrigin(value, environment) {
553
+ if (typeof value !== "string" || !value.trim()) {
554
+ throw new Error(`lmt-web.config.json does not contain the ${environment} Web URL.`);
555
+ }
556
+ let origin;
557
+ try {
558
+ origin = new URL(value.trim());
559
+ } catch {
560
+ throw new Error(`${environment} Web URL must be an absolute HTTP URL.`);
561
+ }
562
+ if (!/^https?:$/.test(origin.protocol) || origin.username || origin.password || origin.hash) {
563
+ throw new Error(
564
+ `${environment} Web URL must be an HTTP URL without credentials or a fragment.`
565
+ );
566
+ }
567
+ if (origin.pathname !== "/" || origin.search) {
568
+ throw new Error(`${environment} Web URL must not include a path or query string.`);
569
+ }
570
+ return origin.origin;
571
+ }
572
+ function resolveWebTarget(environmentValue, configPath = process.env.LMT_WEB_CONFIG_PATH || DEFAULT_CONFIG_PATH2) {
573
+ const environment = environmentValue?.trim();
574
+ if (environment !== "local" && environment !== "dev") {
575
+ if (environment === "prod") {
576
+ throw new Error("lmt-web supports local or dev targets only; prod is not permitted.");
577
+ }
578
+ throw new Error("Target must explicitly select --env local or --env dev; prod is not permitted.");
579
+ }
580
+ let config;
581
+ try {
582
+ config = JSON.parse(readFileSync2(configPath, "utf8"));
583
+ } catch {
584
+ throw new Error("Could not read the deployment configuration lmt-web.config.json.");
585
+ }
586
+ const selected = config.environments?.[environment];
587
+ if (!selected) {
588
+ throw new Error(`lmt-web.config.json does not contain the ${environment} environment.`);
589
+ }
590
+ return { environment, origin: canonicalOrigin(selected.origin, environment) };
591
+ }
592
+
593
+ // src/protocol.ts
594
+ var RESULT_VERSION = 1;
595
+ function failureResult(action, code, message, sessionId = null) {
596
+ return {
597
+ version: RESULT_VERSION,
598
+ sessionId,
599
+ action,
600
+ outcome: "failure",
601
+ url: null,
602
+ visibleState: null,
603
+ device: null,
604
+ screenshot: { outcome: "unavailable", reason: "No browser page was available." },
605
+ error: { code, message }
606
+ };
607
+ }
608
+
609
+ // src/session.ts
610
+ import { randomBytes as randomBytes2 } from "node:crypto";
611
+ import { chmod as chmod2, lstat as lstat2, mkdir as mkdir2, unlink as unlink2 } from "node:fs/promises";
612
+ import net from "node:net";
613
+ import os from "node:os";
614
+ import path4 from "node:path";
615
+ import { chromium as chromium2 } from "playwright";
616
+
617
+ // ../../packages/crawler/src/controls.ts
618
+ import { randomBytes } from "node:crypto";
619
+ var ControlError = class extends Error {
620
+ constructor(code, message) {
621
+ super(message);
622
+ this.code = code;
623
+ }
624
+ code;
625
+ };
626
+ function controlSignature(node) {
627
+ if (!node.isConnected) return null;
628
+ return JSON.stringify([
629
+ node.tagName,
630
+ node.getAttribute("type"),
631
+ node.getAttribute("name"),
632
+ node.getAttribute("aria-label"),
633
+ node.getAttribute("href"),
634
+ node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement ? "" : node.textContent
635
+ ]);
636
+ }
637
+ var PageControls = class {
638
+ constructor(page, origin) {
639
+ this.origin = origin;
640
+ this.page = page;
641
+ }
642
+ origin;
643
+ page;
644
+ references = /* @__PURE__ */ new Map();
645
+ secrets = /* @__PURE__ */ new Set();
646
+ prefix = randomBytes(8).toString("hex");
647
+ sequence = 0;
648
+ registerSecrets(...values) {
649
+ for (const value of values) if (value) this.secrets.add(value);
650
+ }
651
+ redact(value) {
652
+ let result = value;
653
+ for (const secret of [...this.secrets].sort((a, b) => b.length - a.length)) {
654
+ result = result.split(secret).join("[redacted]");
655
+ result = result.split(encodeURIComponent(secret)).join("[redacted]");
656
+ }
657
+ return result;
658
+ }
659
+ async learnSecrets() {
660
+ const values = await this.page.evaluate(() => {
661
+ const sensitive = /password|credential|secret|token|otp|one.time|username|user.?id|api.?key|pin/i;
662
+ const found = Array.from(document.querySelectorAll("input, textarea")).filter((element) => sensitive.test([
663
+ element.outerHTML.replace(/value=("[^"]*"|'[^']*')/g, ""),
664
+ ...Array.from(element.labels ?? []).map((label) => label.innerText)
665
+ ].join(" "))).map((element) => element.value).filter(Boolean);
666
+ for (const match of (document.body?.innerText ?? "").matchAll(/(?:access[\s_-]*token|request[\s_-]*token|api[\s_-]*(?:secret|key|token)|password|credential|otp)(?:\s*[:=]\s*|\s*\n\s*)([^\s,;]+)/gi)) {
667
+ if (match[1]) found.push(match[1]);
668
+ }
669
+ return found;
670
+ });
671
+ this.registerSecrets(...values);
672
+ }
673
+ async screenshotMasks() {
674
+ try {
675
+ await this.learnSecrets();
676
+ } catch (error) {
677
+ if (this.secrets.size) throw error;
678
+ else return [];
679
+ }
680
+ await this.page.evaluate((secrets) => {
681
+ document.querySelectorAll("[data-lmt-secret-mask]").forEach((element) => element.removeAttribute("data-lmt-secret-mask"));
682
+ if (!secrets.length) return;
683
+ for (const element of document.querySelectorAll("body *")) {
684
+ const directText = Array.from(element.childNodes).filter((node) => node.nodeType === Node.TEXT_NODE).map((node) => node.textContent).join("");
685
+ if (secrets.some((secret) => directText.includes(secret) || (element.textContent ?? "").includes(secret) && !Array.from(element.children).some((child) => (child.textContent ?? "").includes(secret)))) element.setAttribute("data-lmt-secret-mask", "");
686
+ }
687
+ }, [...this.secrets]);
688
+ return [this.page.locator("[data-lmt-secret-mask]")];
689
+ }
690
+ async inspect() {
691
+ await this.learnSecrets();
692
+ for (const { element } of this.references.values()) await element.dispose().catch(() => void 0);
693
+ this.references.clear();
694
+ const controls = [];
695
+ const elements = await this.page.$$("button, a[href], input, select, textarea, [role=button], [contenteditable=true]");
696
+ for (const element of elements) {
697
+ if (controls.length >= 100 || !await element.isVisible()) {
698
+ await element.dispose();
699
+ continue;
700
+ }
701
+ const description = await element.evaluate((node) => {
702
+ const labels = Array.from(node.labels ?? []).map((label) => {
703
+ const copy = label.cloneNode(true);
704
+ copy.querySelectorAll("input,select,textarea,button").forEach((control) => control.remove());
705
+ return copy.textContent ?? "";
706
+ }).join(" ");
707
+ return {
708
+ kind: node.getAttribute("role") || node.tagName.toLowerCase(),
709
+ label: (node.getAttribute("aria-label") || node.getAttribute("title") || labels || node.getAttribute("placeholder") || (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement ? "" : node.textContent) || "").replace(/\s+/g, " ").trim()
710
+ };
711
+ });
712
+ const reference = `${this.prefix}-${++this.sequence}`;
713
+ this.references.set(reference, { element, signature: await element.evaluate(controlSignature) });
714
+ controls.push({ reference, kind: description.kind, label: this.redact(description.label).slice(0, 160) });
715
+ }
716
+ return controls;
717
+ }
718
+ async resolve(reference) {
719
+ const saved = this.references.get(reference);
720
+ const element = saved?.element;
721
+ if (!element || !saved?.signature || await element.evaluate(controlSignature).catch(() => null) !== saved.signature) {
722
+ throw new ControlError("stale_reference", "The control reference is missing or stale. Inspect the page for current references.");
723
+ }
724
+ if (!await element.isVisible()) throw new ControlError("stale_reference", "The referenced control is no longer visible. Inspect the page again.");
725
+ return element;
726
+ }
727
+ async stopLoading() {
728
+ const session = await this.page.context().newCDPSession(this.page);
729
+ try {
730
+ await session.send("Page.stopLoading");
731
+ } finally {
732
+ await session.detach();
733
+ }
734
+ }
735
+ async act(command, timeoutMs, onSubmit = () => void 0) {
736
+ this.page.setDefaultTimeout(timeoutMs);
737
+ this.page.setDefaultNavigationTimeout(timeoutMs);
738
+ if (command.action === "fill") {
739
+ const element = await this.resolve(command.reference);
740
+ const sensitive = await element.evaluate((node) => /password|credential|secret|token|otp|one.time|username|user.?id|api.?key|pin/i.test(node.outerHTML + Array.from(node.labels ?? []).map((label) => label.innerText).join(" ")));
741
+ if (command.secret || sensitive) this.registerSecrets(command.value);
742
+ await element.fill(command.value, { timeout: timeoutMs });
743
+ return;
744
+ }
745
+ if (command.action === "select") {
746
+ await (await this.resolve(command.reference)).selectOption(command.value, { timeout: timeoutMs });
747
+ return;
748
+ }
749
+ if (command.action === "click") {
750
+ const element = await this.resolve(command.reference);
751
+ const deadlineAt = Date.now() + timeoutMs;
752
+ await element.click({ trial: true, timeout: timeoutMs });
753
+ onSubmit();
754
+ try {
755
+ await element.click({ timeout: Math.max(1, deadlineAt - Date.now()) });
756
+ } catch {
757
+ throw new ControlError("action_uncertain", "The click may have been submitted but its completion was not confirmed. Inspect before deciding whether to retry.");
758
+ }
759
+ return;
760
+ }
761
+ if (command.action === "navigate") {
762
+ const target = new URL(command.url, this.origin);
763
+ if (target.origin !== new URL(this.origin).origin || target.username || target.password) {
764
+ throw new ControlError("invalid_target", "Navigation must use the session's configured Origin.");
765
+ }
766
+ await this.page.goto(target.href, { timeout: timeoutMs, waitUntil: "domcontentloaded" });
767
+ return;
768
+ }
769
+ if (command.action === "scroll") {
770
+ if (!Number.isFinite(command.x) || !Number.isFinite(command.y)) throw new ControlError("invalid_invocation", "Scroll offsets must be finite pixel values.");
771
+ if (command.reference) {
772
+ await (await this.resolve(command.reference)).evaluate((node, offsets) => node.scrollBy({ left: offsets.x, top: offsets.y, behavior: "instant" }), command);
773
+ } else {
774
+ await this.page.evaluate((offsets) => window.scrollBy({ left: offsets.x, top: offsets.y, behavior: "instant" }), command);
775
+ }
776
+ await this.page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
777
+ }
778
+ }
779
+ };
780
+
781
+ // ../../packages/crawler/src/index.ts
782
+ import {
783
+ chromium
784
+ } from "playwright";
785
+ var DEFAULT_BASE_URL = "http://127.0.0.1:5173";
786
+ var DEFAULT_TIMEOUT_MS = 3e4;
787
+ var DEFAULT_INTERACTIVE_LOGIN_TIMEOUT_MS = 5 * 6e4;
788
+ var OPERATOR_PAGE_SELECTOR = '[data-operator-page="true"]';
789
+ var MARKET_WATCH_SELECTOR = '[data-market-watch="true"]';
790
+ var OPERATOR_LOGIN_GATE_SELECTOR = '[data-operator-login-gate="true"]';
791
+ var OPERATOR_LOGIN_USERNAME_SELECTOR = "[data-operator-login-username]";
792
+ var OPERATOR_LOGIN_PASSWORD_SELECTOR = "[data-operator-login-password]";
793
+ var OPERATOR_LOGIN_CONFIRM_SELECTOR = "[data-operator-login-confirm]";
794
+ var CREDENTIALS_STATUS_SELECTOR = "[data-credentials-status]";
795
+ var ZERODHA_LOGIN_PAGE_SELECTOR = '[data-zerodha-login-page="true"]';
796
+ var OPERATOR_LOGIN_ATTEMPT_SELECTOR = "[data-operator-login-attempt]";
797
+ var OPERATOR_LOGIN_STATUS_ATTRIBUTE = "data-operator-login-status";
798
+ var OPERATOR_LOGIN_STATUS_MESSAGE_SELECTOR = '[data-operator-login-message-kind="status"]';
799
+ var OPERATOR_LOGIN_ERROR_MESSAGE_SELECTOR = '[data-operator-login-message-kind="error"]';
800
+ var OPERATOR_LOGIN_MESSAGE_ATTRIBUTE = "data-operator-login-message";
801
+ var OPERATOR_LOGIN_OTP_SELECTOR = "[data-operator-login-otp]";
802
+ var OPERATOR_LOGIN_OTP_SUBMIT_SELECTOR = "[data-operator-login-otp-submit]";
803
+ var AUTHENTICATED_OPERATOR_LOGOUT_SELECTOR = "[data-authenticated-operator-logout]";
804
+ var UNAUTHENTICATED_OPERATOR_LOGOUT_SELECTOR = "[data-unauthenticated-operator-logout]";
805
+ var SETTINGS_SELECTOR = "[data-zc-settings]";
806
+ var OPERATOR_EGRESS_SELECTOR = "[data-operator-egress-value]";
807
+ var PEEK_TIMEOUT_MS = 250;
808
+ function sleep(ms) {
809
+ return new Promise((resolve) => setTimeout(resolve, ms));
810
+ }
811
+ async function isAttached(page, selector, attributeName) {
812
+ if (typeof page.waitForSelector === "function") {
813
+ try {
814
+ await page.waitForSelector(selector, { timeout: PEEK_TIMEOUT_MS });
815
+ return true;
816
+ } catch {
817
+ return false;
818
+ }
819
+ }
820
+ const value = await peekAttribute(page, selector, attributeName);
821
+ return value !== null;
822
+ }
823
+ async function peekAttribute(page, selector, name) {
824
+ try {
825
+ return await page.locator(selector).getAttribute(name, { timeout: PEEK_TIMEOUT_MS });
826
+ } catch {
827
+ return null;
828
+ }
829
+ }
830
+ function marketWatchRowSelector(sno) {
831
+ return `[data-market-watch-row="${sno}"]`;
832
+ }
833
+ function marketWatchExchangeSelector(sno) {
834
+ return `[data-market-watch-exchange="${sno}"]`;
835
+ }
836
+ function marketWatchTradingSymbolSelector(sno) {
837
+ return `[data-market-watch-trading-symbol="${sno}"]`;
838
+ }
839
+ function marketWatchApplySelector(sno) {
840
+ return `[data-market-watch-apply="${sno}"]`;
841
+ }
842
+ function marketWatchClearSelector(sno) {
843
+ return `[data-market-watch-clear="${sno}"]`;
844
+ }
845
+ function marketWatchCommittedSelector(sno) {
846
+ return `[data-market-watch-committed="${sno}"]`;
847
+ }
848
+ function marketWatchQuoteSelector(sno) {
849
+ return `[data-market-watch-quote="${sno}"]`;
850
+ }
851
+ function marketWatchQuoteFieldSelector(sno, field) {
852
+ return `${marketWatchQuoteSelector(sno)} [data-market-watch-quote-field="${field}"]`;
853
+ }
854
+ var QUOTE_FIELDS = {
855
+ ltp: "lastPrice",
856
+ open: "open",
857
+ high: "high",
858
+ low: "low",
859
+ close: "close",
860
+ volume: "volume",
861
+ "percentage-change": "percentageChange"
862
+ };
863
+ function toMarketWatchRowStatus(raw) {
864
+ switch (raw) {
865
+ case "Empty":
866
+ return "empty";
867
+ case "Validating":
868
+ return "validating";
869
+ case "Live":
870
+ return "live";
871
+ case "Error":
872
+ return "error";
873
+ case "Configured":
874
+ return "stopped";
875
+ case null:
876
+ throw new Error("Market Watch row did not expose a state");
877
+ default:
878
+ throw new Error(`Unknown Market Watch row state: ${raw}`);
879
+ }
880
+ }
881
+ var PlaywrightOperatorPage = class {
882
+ constructor(page, timeoutMs = DEFAULT_TIMEOUT_MS, interactiveLoginTimeoutMs = DEFAULT_INTERACTIVE_LOGIN_TIMEOUT_MS, headless = true, registerCleanup = () => void 0) {
883
+ this.page = page;
884
+ this.timeoutMs = timeoutMs;
885
+ this.interactiveLoginTimeoutMs = interactiveLoginTimeoutMs;
886
+ this.headless = headless;
887
+ this.registerCleanup = registerCleanup;
888
+ }
889
+ page;
890
+ timeoutMs;
891
+ interactiveLoginTimeoutMs;
892
+ headless;
893
+ registerCleanup;
894
+ async getOperatorInfo() {
895
+ await this.page.waitForSelector?.(OPERATOR_PAGE_SELECTOR, {
896
+ timeout: this.timeoutMs
897
+ });
898
+ const root = this.page.locator(OPERATOR_PAGE_SELECTOR);
899
+ const userId = (await root.getAttribute("data-user-id"))?.trim() ?? "";
900
+ const userName = (await root.getAttribute("data-user-name"))?.trim() ?? "";
901
+ if (!userId) {
902
+ throw new Error("Operator Page did not expose a user id");
903
+ }
904
+ return {
905
+ userId,
906
+ userName: userName || userId
907
+ };
908
+ }
909
+ async getMarketWatchRowCount() {
910
+ await this.page.waitForSelector?.(OPERATOR_PAGE_SELECTOR, {
911
+ timeout: this.timeoutMs
912
+ });
913
+ const raw = await this.page.locator(MARKET_WATCH_SELECTOR).getAttribute("data-market-watch-row-count", { timeout: this.timeoutMs });
914
+ const rowCount = Number(raw);
915
+ if (!Number.isInteger(rowCount) || rowCount < 1) {
916
+ throw new Error(`Market Watch exposed invalid row count: ${String(raw)}`);
917
+ }
918
+ return rowCount;
919
+ }
920
+ async getMarketWatchRowNumbers() {
921
+ const rowCount = await this.getMarketWatchRowCount();
922
+ const rowNumbers = Array.from({ length: rowCount }, (_, index) => index + 1);
923
+ for (const sno of rowNumbers) {
924
+ await this.page.waitForSelector?.(marketWatchRowSelector(sno), {
925
+ timeout: this.timeoutMs
926
+ });
927
+ }
928
+ return rowNumbers;
929
+ }
930
+ async getMarketWatchRow(sno) {
931
+ await this.page.waitForSelector?.(OPERATOR_PAGE_SELECTOR, {
932
+ timeout: this.timeoutMs
933
+ });
934
+ const row = this.page.locator(marketWatchRowSelector(sno));
935
+ await this.page.waitForSelector?.(marketWatchRowSelector(sno), {
936
+ timeout: this.timeoutMs
937
+ });
938
+ const committed = this.page.locator(marketWatchCommittedSelector(sno));
939
+ const committedAttribute = (name2) => committed.getAttribute(name2, { timeout: PEEK_TIMEOUT_MS }).catch(() => null);
940
+ const match = (await committedAttribute("data-market-watch-match"))?.trim();
941
+ const token = (await committedAttribute("data-market-watch-token"))?.trim();
942
+ const name = (await committedAttribute("data-market-watch-name"))?.trim();
943
+ const segment = (await committedAttribute("data-market-watch-segment"))?.trim();
944
+ const draftError = (await this.page.locator(`[data-market-watch-draft-error="${sno}"]`).getAttribute("data-market-watch-draft-error-message", {
945
+ timeout: PEEK_TIMEOUT_MS
946
+ }).catch(() => null))?.trim();
947
+ const quote = match && token ? await this.readMarketWatchQuote(sno, token) : void 0;
948
+ return {
949
+ sno,
950
+ exchange: await this.page.locator(marketWatchExchangeSelector(sno)).inputValue(),
951
+ tradingSymbol: await this.page.locator(marketWatchTradingSymbolSelector(sno)).inputValue(),
952
+ status: toMarketWatchRowStatus(
953
+ await row.getAttribute("data-market-watch-row-state")
954
+ ),
955
+ ...draftError ? { draftError } : {},
956
+ failureMarked: await row.getAttribute("data-market-watch-row-failure") === "true",
957
+ ...quote ? { quote } : {},
958
+ ...match && token ? {
959
+ instrumentInfo: {
960
+ match,
961
+ token,
962
+ name: name ?? "",
963
+ segment: segment ?? ""
964
+ },
965
+ onTick: (listener) => {
966
+ const unsubscribe = this.subscribeLiveTicks(sno, token, listener);
967
+ this.registerCleanup(unsubscribe);
968
+ return unsubscribe;
969
+ }
970
+ } : {}
971
+ };
972
+ }
973
+ async setMarketWatchRow(sno, exchange, tradingSymbol) {
974
+ await this.page.waitForSelector?.(OPERATOR_PAGE_SELECTOR, {
975
+ timeout: this.timeoutMs
976
+ });
977
+ await this.page.locator(marketWatchExchangeSelector(sno)).selectOption(exchange.trim().toUpperCase());
978
+ await this.page.locator(marketWatchTradingSymbolSelector(sno)).fill(tradingSymbol);
979
+ }
980
+ async applyMarketWatchRow(sno) {
981
+ await this.page.waitForSelector?.(OPERATOR_PAGE_SELECTOR, {
982
+ timeout: this.timeoutMs
983
+ });
984
+ await this.page.locator(marketWatchApplySelector(sno)).click();
985
+ await this.waitForMarketWatchRowState(
986
+ sno,
987
+ (state) => state === "Error" || state === "Live"
988
+ );
989
+ }
990
+ async clearMarketWatchRow(sno) {
991
+ await this.page.waitForSelector?.(OPERATOR_PAGE_SELECTOR, {
992
+ timeout: this.timeoutMs
993
+ });
994
+ await this.page.locator(marketWatchClearSelector(sno)).click();
995
+ await this.waitForMarketWatchRowState(sno, (state) => state === "Empty");
996
+ }
997
+ async refresh() {
998
+ await this.page.reload();
999
+ await this.page.waitForSelector?.(OPERATOR_PAGE_SELECTOR, {
1000
+ timeout: this.timeoutMs
1001
+ });
1002
+ await this.page.waitForSelector?.(MARKET_WATCH_SELECTOR, {
1003
+ timeout: this.timeoutMs
1004
+ });
1005
+ }
1006
+ async waitForAttribute(selector, attribute, predicate, description) {
1007
+ const element = this.page.locator(selector);
1008
+ const deadline = Date.now() + this.timeoutMs;
1009
+ while (Date.now() < deadline) {
1010
+ const value = await element.getAttribute(attribute, { timeout: PEEK_TIMEOUT_MS }).catch(() => null);
1011
+ if (predicate(value)) {
1012
+ return;
1013
+ }
1014
+ await sleep(50);
1015
+ }
1016
+ throw new Error(`Timed out waiting for ${description}`);
1017
+ }
1018
+ async waitForMarketWatchRowState(sno, predicate) {
1019
+ await this.waitForAttribute(
1020
+ marketWatchRowSelector(sno),
1021
+ "data-market-watch-row-state",
1022
+ predicate,
1023
+ `Market Watch row ${sno} state`
1024
+ );
1025
+ }
1026
+ async readMarketWatchQuote(sno, fallbackToken) {
1027
+ const root = this.page.locator(marketWatchQuoteSelector(sno));
1028
+ const receivedAt = await root.getAttribute("data-market-watch-quote-received-at", {
1029
+ timeout: PEEK_TIMEOUT_MS
1030
+ }).catch(() => null);
1031
+ if (!receivedAt) return void 0;
1032
+ const receivedOn = new Date(receivedAt);
1033
+ if (Number.isNaN(receivedOn.getTime())) {
1034
+ throw new Error(`Market Watch row ${sno} exposed an invalid quote timestamp`);
1035
+ }
1036
+ const exchangeTimestampRaw = await root.getAttribute("data-market-watch-quote-exchange-time", {
1037
+ timeout: PEEK_TIMEOUT_MS
1038
+ }).catch(() => null);
1039
+ const exchangeTimestamp = exchangeTimestampRaw ? new Date(exchangeTimestampRaw) : void 0;
1040
+ if (exchangeTimestamp && Number.isNaN(exchangeTimestamp.getTime())) {
1041
+ throw new Error(
1042
+ `Market Watch row ${sno} exposed an invalid exchange tick timestamp`
1043
+ );
1044
+ }
1045
+ const timestampStatusRaw = await root.getAttribute("data-market-watch-quote-timestamp-status", {
1046
+ timeout: PEEK_TIMEOUT_MS
1047
+ }).catch(() => null);
1048
+ const missingTimestampCountRaw = await root.getAttribute("data-market-watch-quote-missing-timestamp-count", {
1049
+ timeout: PEEK_TIMEOUT_MS
1050
+ }).catch(() => null);
1051
+ const updateTypeRaw = await root.getAttribute("data-market-watch-quote-update-type", {
1052
+ timeout: PEEK_TIMEOUT_MS
1053
+ }).catch(() => null);
1054
+ const missingTimestampCount = Number(missingTimestampCountRaw);
1055
+ if (missingTimestampCountRaw !== null && (!Number.isInteger(missingTimestampCount) || missingTimestampCount < 0)) {
1056
+ throw new Error(
1057
+ `Market Watch row ${sno} exposed an invalid missing timestamp count`
1058
+ );
1059
+ }
1060
+ return {
1061
+ receivedAt: receivedOn,
1062
+ token: (await root.getAttribute("data-market-watch-quote-token", {
1063
+ timeout: PEEK_TIMEOUT_MS
1064
+ }).catch(() => null))?.trim() || fallbackToken,
1065
+ mode: (await root.getAttribute("data-market-watch-quote-mode", {
1066
+ timeout: PEEK_TIMEOUT_MS
1067
+ }).catch(() => null))?.trim() || "full",
1068
+ ...updateTypeRaw === "liveTick" || updateTypeRaw === "quoteSnapshot" || updateTypeRaw === "reconnectSnapshot" ? { latestUpdateType: updateTypeRaw } : {},
1069
+ ...exchangeTimestamp ? { exchangeTimestamp } : {},
1070
+ ...timestampStatusRaw === "available" || timestampStatusRaw === "missing" || timestampStatusRaw === "invalid" ? { exchangeTimestampStatus: timestampStatusRaw } : {},
1071
+ ...missingTimestampCountRaw !== null ? { missingExchangeTimestampCount: missingTimestampCount } : {},
1072
+ fields: await this.readQuoteFields(sno)
1073
+ };
1074
+ }
1075
+ async readQuoteFields(sno) {
1076
+ const fields = {};
1077
+ for (const [domField, property] of Object.entries(QUOTE_FIELDS)) {
1078
+ const raw = await this.page.locator(marketWatchQuoteFieldSelector(sno, domField)).getAttribute("data-market-watch-quote-value", {
1079
+ timeout: PEEK_TIMEOUT_MS
1080
+ }).catch(() => null);
1081
+ if (raw === null || raw.trim() === "") continue;
1082
+ const value = Number(raw);
1083
+ if (!Number.isFinite(value)) {
1084
+ throw new Error(
1085
+ `Market Watch row ${sno} exposed a non-numeric ${domField} quote value`
1086
+ );
1087
+ }
1088
+ fields[property] = value;
1089
+ }
1090
+ return fields;
1091
+ }
1092
+ subscribeLiveTicks(sno, expectedToken, listener) {
1093
+ let stopped = false;
1094
+ let lastFingerprint = "";
1095
+ const poll = async () => {
1096
+ while (!stopped) {
1097
+ const quoteRoot = this.page.locator(marketWatchQuoteSelector(sno));
1098
+ const receivedOnRaw = await quoteRoot.getAttribute("data-market-watch-quote-received-at", {
1099
+ timeout: PEEK_TIMEOUT_MS
1100
+ }).catch(() => null);
1101
+ const token = await quoteRoot.getAttribute("data-market-watch-quote-token", {
1102
+ timeout: PEEK_TIMEOUT_MS
1103
+ }).catch(() => null);
1104
+ const mode = await quoteRoot.getAttribute("data-market-watch-quote-mode", {
1105
+ timeout: PEEK_TIMEOUT_MS
1106
+ }).catch(() => null);
1107
+ const exchangeTimestampRaw = await quoteRoot.getAttribute("data-market-watch-quote-exchange-time", {
1108
+ timeout: PEEK_TIMEOUT_MS
1109
+ }).catch(() => null);
1110
+ const timestampStatusRaw = await quoteRoot.getAttribute("data-market-watch-quote-timestamp-status", {
1111
+ timeout: PEEK_TIMEOUT_MS
1112
+ }).catch(() => null);
1113
+ const missingTimestampCountRaw = await quoteRoot.getAttribute("data-market-watch-quote-missing-timestamp-count", {
1114
+ timeout: PEEK_TIMEOUT_MS
1115
+ }).catch(() => null);
1116
+ const updateTypeRaw = await quoteRoot.getAttribute("data-market-watch-quote-update-type", {
1117
+ timeout: PEEK_TIMEOUT_MS
1118
+ }).catch(() => null);
1119
+ if (receivedOnRaw && token === expectedToken) {
1120
+ const actualMode = mode?.trim() || "";
1121
+ const fingerprint = `${receivedOnRaw}|${token}|${actualMode}|${updateTypeRaw ?? ""}|${exchangeTimestampRaw ?? ""}|${timestampStatusRaw ?? ""}`;
1122
+ if (fingerprint !== lastFingerprint) {
1123
+ lastFingerprint = fingerprint;
1124
+ const fields = await this.readQuoteFields(sno);
1125
+ const exchangeTimestamp = exchangeTimestampRaw ? new Date(exchangeTimestampRaw) : void 0;
1126
+ const missingExchangeTimestampCount = Number(
1127
+ missingTimestampCountRaw
1128
+ );
1129
+ const tick = {
1130
+ receivedOn: new Date(receivedOnRaw),
1131
+ token,
1132
+ mode: actualMode,
1133
+ ...exchangeTimestamp && !Number.isNaN(exchangeTimestamp.getTime()) ? { exchangeTimestamp } : {},
1134
+ ...timestampStatusRaw === "available" || timestampStatusRaw === "missing" || timestampStatusRaw === "invalid" ? { exchangeTimestampStatus: timestampStatusRaw } : {},
1135
+ ...Number.isInteger(missingExchangeTimestampCount) && missingExchangeTimestampCount >= 0 ? { missingExchangeTimestampCount } : {},
1136
+ ...fields
1137
+ };
1138
+ if (tick.mode === "full" && updateTypeRaw === "liveTick" && timestampStatusRaw === "available" && tick.exchangeTimestamp && typeof tick.lastPrice === "number" && Number.isFinite(tick.lastPrice) && tick.lastPrice > 0) {
1139
+ listener(tick);
1140
+ }
1141
+ }
1142
+ }
1143
+ await sleep(50);
1144
+ }
1145
+ };
1146
+ void poll();
1147
+ return () => {
1148
+ stopped = true;
1149
+ };
1150
+ }
1151
+ async clickLogoutZerodha(onSubmit = () => void 0) {
1152
+ await this.page.waitForSelector?.(OPERATOR_PAGE_SELECTOR, {
1153
+ timeout: this.timeoutMs
1154
+ });
1155
+ await this.page.locator("[data-operator-info-toggle]").click();
1156
+ onSubmit();
1157
+ await this.page.locator(AUTHENTICATED_OPERATOR_LOGOUT_SELECTOR).click();
1158
+ await this.page.waitForSelector?.(ZERODHA_LOGIN_PAGE_SELECTOR, {
1159
+ timeout: this.timeoutMs
1160
+ });
1161
+ return new PlaywrightZerodhaLoginPage(
1162
+ this.page,
1163
+ this.timeoutMs,
1164
+ this.interactiveLoginTimeoutMs,
1165
+ this.headless,
1166
+ this.registerCleanup
1167
+ );
1168
+ }
1169
+ };
1170
+ var LoginAttemptError = class extends Error {
1171
+ constructor(code, message) {
1172
+ super(message);
1173
+ this.code = code;
1174
+ }
1175
+ code;
1176
+ };
1177
+ var PlaywrightZerodhaLoginPage = class {
1178
+ constructor(page, timeoutMs = DEFAULT_TIMEOUT_MS, interactiveLoginTimeoutMs = DEFAULT_INTERACTIVE_LOGIN_TIMEOUT_MS, headless = true, registerCleanup = () => void 0) {
1179
+ this.page = page;
1180
+ this.timeoutMs = timeoutMs;
1181
+ this.interactiveLoginTimeoutMs = interactiveLoginTimeoutMs;
1182
+ this.headless = headless;
1183
+ this.registerCleanup = registerCleanup;
1184
+ }
1185
+ page;
1186
+ timeoutMs;
1187
+ interactiveLoginTimeoutMs;
1188
+ headless;
1189
+ registerCleanup;
1190
+ async getOperatorEgress() {
1191
+ await this.page.waitForSelector?.(ZERODHA_LOGIN_PAGE_SELECTOR, {
1192
+ timeout: this.timeoutMs
1193
+ });
1194
+ if (!await isAttached(
1195
+ this.page,
1196
+ OPERATOR_EGRESS_SELECTOR,
1197
+ "data-operator-egress-value"
1198
+ )) {
1199
+ await this.page.locator(SETTINGS_SELECTOR).click();
1200
+ }
1201
+ await this.page.waitForSelector?.(OPERATOR_EGRESS_SELECTOR, {
1202
+ timeout: this.timeoutMs
1203
+ });
1204
+ const field = this.page.locator(OPERATOR_EGRESS_SELECTOR);
1205
+ return {
1206
+ publicIp: await field.inputValue(),
1207
+ readOnly: await field.getAttribute("readonly") !== null
1208
+ };
1209
+ }
1210
+ async peekLoginStatus() {
1211
+ return peekAttribute(
1212
+ this.page,
1213
+ OPERATOR_LOGIN_ATTEMPT_SELECTOR,
1214
+ OPERATOR_LOGIN_STATUS_ATTRIBUTE
1215
+ );
1216
+ }
1217
+ async peekLoginMessage() {
1218
+ return await this.peekLoginStatusMessage() ?? await this.peekLoginErrorMessage();
1219
+ }
1220
+ async peekLoginStatusMessage() {
1221
+ const message = await peekAttribute(
1222
+ this.page,
1223
+ OPERATOR_LOGIN_STATUS_MESSAGE_SELECTOR,
1224
+ OPERATOR_LOGIN_MESSAGE_ATTRIBUTE
1225
+ );
1226
+ return message?.trim() || null;
1227
+ }
1228
+ async peekLoginErrorMessage() {
1229
+ const message = await peekAttribute(
1230
+ this.page,
1231
+ OPERATOR_LOGIN_ERROR_MESSAGE_SELECTOR,
1232
+ OPERATOR_LOGIN_MESSAGE_ATTRIBUTE
1233
+ );
1234
+ return message?.trim() || null;
1235
+ }
1236
+ async waitForLoginStatus(predicate, description) {
1237
+ const deadline = Date.now() + this.interactiveLoginTimeoutMs;
1238
+ while (Date.now() < deadline) {
1239
+ const status = await this.peekLoginStatus();
1240
+ if (predicate(status)) {
1241
+ return;
1242
+ }
1243
+ const message = await this.peekLoginMessage();
1244
+ if (status === "failed" || status === null && message) {
1245
+ throw new Error(message ?? "Zerodha Login Attempt failed.");
1246
+ }
1247
+ await sleep(50);
1248
+ }
1249
+ throw new Error(`Timed out waiting for ${description}`);
1250
+ }
1251
+ async waitForLoginAttempt() {
1252
+ const deadline = Date.now() + this.interactiveLoginTimeoutMs;
1253
+ while (Date.now() < deadline) {
1254
+ if (await isAttached(
1255
+ this.page,
1256
+ OPERATOR_LOGIN_ATTEMPT_SELECTOR,
1257
+ "data-operator-login-attempt"
1258
+ )) {
1259
+ return;
1260
+ }
1261
+ const message = await this.peekLoginMessage();
1262
+ if (message) {
1263
+ throw new Error(message);
1264
+ }
1265
+ await sleep(50);
1266
+ }
1267
+ throw new Error("Timed out waiting for the Zerodha Login Attempt");
1268
+ }
1269
+ /** Start Zerodha Login Attempt for this Operator Session (always OTP path). */
1270
+ async clickLoginWithZerodha() {
1271
+ await this.page.waitForSelector?.(ZERODHA_LOGIN_PAGE_SELECTOR, {
1272
+ timeout: this.timeoutMs
1273
+ });
1274
+ if (await isAttached(
1275
+ this.page,
1276
+ OPERATOR_LOGIN_ATTEMPT_SELECTOR,
1277
+ "data-operator-login-attempt"
1278
+ )) {
1279
+ return;
1280
+ }
1281
+ await this.page.locator(OPERATOR_LOGIN_CONFIRM_SELECTOR).click();
1282
+ await this.waitForLoginAttempt();
1283
+ }
1284
+ async enterOtp(otp) {
1285
+ await this.clickLoginWithZerodha();
1286
+ await this.waitForLoginStatus(
1287
+ (status) => status === "awaiting_otp",
1288
+ "the Zerodha OTP Challenge"
1289
+ );
1290
+ await this.page.locator(OPERATOR_LOGIN_OTP_SELECTOR).fill(otp);
1291
+ }
1292
+ async submitOtp() {
1293
+ const previousStatusMessage = await this.peekLoginStatusMessage();
1294
+ await this.page.locator(OPERATOR_LOGIN_OTP_SUBMIT_SELECTOR).click();
1295
+ const deadline = Date.now() + this.interactiveLoginTimeoutMs;
1296
+ while (Date.now() < deadline) {
1297
+ if (await isAttached(this.page, OPERATOR_PAGE_SELECTOR, "data-operator-page")) {
1298
+ return {
1299
+ status: "SUCCESS",
1300
+ operatorPage: new PlaywrightOperatorPage(
1301
+ this.page,
1302
+ this.timeoutMs,
1303
+ this.interactiveLoginTimeoutMs,
1304
+ this.headless,
1305
+ this.registerCleanup
1306
+ )
1307
+ };
1308
+ }
1309
+ const status = await this.peekLoginStatus();
1310
+ const errorMessage = await this.peekLoginErrorMessage();
1311
+ if (errorMessage) {
1312
+ return { status: "FAILED", message: errorMessage };
1313
+ }
1314
+ const statusMessage = await this.peekLoginStatusMessage();
1315
+ if (status === "failed") {
1316
+ return {
1317
+ status: "FAILED",
1318
+ message: statusMessage ?? "Zerodha Login Attempt failed."
1319
+ };
1320
+ }
1321
+ if (statusMessage && statusMessage !== previousStatusMessage) {
1322
+ return { status: "FAILED", message: statusMessage };
1323
+ }
1324
+ await sleep(50);
1325
+ }
1326
+ throw new Error("Timed out waiting for the Zerodha OTP submission result");
1327
+ }
1328
+ async observeLoginAttempt() {
1329
+ if (!this.page.evaluate) throw new Error("Browser inspection is unavailable.");
1330
+ return this.page.evaluate(() => {
1331
+ const attempt = document.querySelector("[data-operator-login-attempt]");
1332
+ const attemptId = attempt?.getAttribute("data-operator-login-attempt-id");
1333
+ if (!attempt || !attemptId) return null;
1334
+ return {
1335
+ attemptId,
1336
+ status: attempt.getAttribute("data-operator-login-status") ?? "unavailable",
1337
+ failedOtpAttempts: Number(attempt.getAttribute("data-operator-login-failed-otp-attempts") ?? 0),
1338
+ submitting: attempt.getAttribute("data-operator-login-submitting-otp") === "true",
1339
+ message: document.querySelector('[data-operator-login-message-kind="error"]')?.getAttribute("data-operator-login-message") ?? null
1340
+ };
1341
+ });
1342
+ }
1343
+ async startLoginAttempt(onSubmit = () => void 0) {
1344
+ const deadline = Date.now() + this.timeoutMs;
1345
+ const existing = await this.observeLoginAttempt();
1346
+ let observedId = existing?.attemptId;
1347
+ if (!existing) {
1348
+ await this.page.waitForSelector?.(ZERODHA_LOGIN_PAGE_SELECTOR, { timeout: this.timeoutMs });
1349
+ onSubmit();
1350
+ await this.page.locator(OPERATOR_LOGIN_CONFIRM_SELECTOR).click();
1351
+ }
1352
+ while (Date.now() < deadline) {
1353
+ const current = await this.observeLoginAttempt();
1354
+ if (observedId && current?.attemptId !== observedId) throw new LoginAttemptError("attempt_replaced", "The Zerodha Login Attempt was replaced. Inspect the page before starting another attempt.");
1355
+ observedId ??= current?.attemptId;
1356
+ if (current?.status === "awaiting_otp") return current;
1357
+ if (current && !["loading", "awaiting_otp", "succeeded", "failed"].includes(current.status)) throw new LoginAttemptError("attempt_unavailable", "The Zerodha Login Attempt is expired or unavailable. No replacement was started.");
1358
+ if (current?.status === "failed") throw new LoginAttemptError("attempt_failed", "The Zerodha Login Attempt failed. Inspect the visible error before starting another attempt.");
1359
+ await sleep(50);
1360
+ }
1361
+ throw new Error("Timed out waiting for the Zerodha OTP Challenge.");
1362
+ }
1363
+ async submitActiveOtp(attemptId, otp, onSubmit = () => void 0) {
1364
+ const deadline = Date.now() + this.timeoutMs;
1365
+ const assertActive = async () => {
1366
+ if (Date.now() >= deadline) throw new Error("OTP submission exceeded its deadline before submission.");
1367
+ const current = await this.observeLoginAttempt();
1368
+ if (!current) throw new LoginAttemptError("attempt_unavailable", "The requested OTP Challenge is unavailable. Inspect the page; no login was started.");
1369
+ if (current.attemptId !== attemptId) throw new LoginAttemptError("attempt_replaced", "The requested OTP Challenge has been replaced. No OTP was submitted.");
1370
+ if (current.status !== "awaiting_otp" || current.submitting) throw new LoginAttemptError("attempt_unavailable", "The requested OTP Challenge is expired, failed, or not accepting an OTP.");
1371
+ return current;
1372
+ };
1373
+ const before = await assertActive();
1374
+ const attemptSelector = `[data-operator-login-attempt-id=${JSON.stringify(attemptId)}]`;
1375
+ await this.page.locator(`${attemptSelector} ${OPERATOR_LOGIN_OTP_SELECTOR}`).fill(otp);
1376
+ await assertActive();
1377
+ onSubmit();
1378
+ await this.page.locator(`${attemptSelector} ${OPERATOR_LOGIN_OTP_SUBMIT_SELECTOR}`).click();
1379
+ while (Date.now() < deadline) {
1380
+ if (await this.page.evaluate?.(() => !!document.querySelector('[data-operator-page="true"]'))) {
1381
+ return { status: "SUCCESS", operatorPage: new PlaywrightOperatorPage(this.page, this.timeoutMs) };
1382
+ }
1383
+ const current = await this.observeLoginAttempt();
1384
+ if (!current) throw new LoginAttemptError("attempt_unavailable", "The OTP Challenge disappeared after submission. Inspect the page before taking another action.");
1385
+ if (current.attemptId !== attemptId) throw new LoginAttemptError("attempt_replaced", "The OTP Challenge was replaced after submission. No retry was performed.");
1386
+ if (current.status === "failed") return { status: "FAILED", message: current.message ?? "Zerodha Login Attempt failed." };
1387
+ if (!current.submitting && current.failedOtpAttempts > before.failedOtpAttempts) return { status: "FAILED", message: current.message ?? "OTP rejected." };
1388
+ await sleep(50);
1389
+ }
1390
+ throw new Error("Timed out waiting for the OTP submission result. No retry was performed.");
1391
+ }
1392
+ async clickLogoutAccount(onSubmit = () => void 0) {
1393
+ await this.page.waitForSelector?.(ZERODHA_LOGIN_PAGE_SELECTOR, {
1394
+ timeout: this.timeoutMs
1395
+ });
1396
+ onSubmit();
1397
+ await this.page.locator(UNAUTHENTICATED_OPERATOR_LOGOUT_SELECTOR).click();
1398
+ await this.page.waitForSelector?.(OPERATOR_LOGIN_GATE_SELECTOR, {
1399
+ timeout: this.timeoutMs
1400
+ });
1401
+ return new PlaywrightHomePage(
1402
+ this.page,
1403
+ this.timeoutMs,
1404
+ this.interactiveLoginTimeoutMs,
1405
+ this.headless,
1406
+ this.registerCleanup
1407
+ );
1408
+ }
1409
+ };
1410
+ var PlaywrightHomePage = class {
1411
+ constructor(page, timeoutMs = DEFAULT_TIMEOUT_MS, interactiveLoginTimeoutMs = DEFAULT_INTERACTIVE_LOGIN_TIMEOUT_MS, headless = true, registerCleanup = () => void 0) {
1412
+ this.page = page;
1413
+ this.timeoutMs = timeoutMs;
1414
+ this.interactiveLoginTimeoutMs = interactiveLoginTimeoutMs;
1415
+ this.headless = headless;
1416
+ this.registerCleanup = registerCleanup;
1417
+ }
1418
+ page;
1419
+ timeoutMs;
1420
+ interactiveLoginTimeoutMs;
1421
+ headless;
1422
+ registerCleanup;
1423
+ async enterUsername(username) {
1424
+ await this.page.waitForSelector?.(OPERATOR_LOGIN_GATE_SELECTOR, {
1425
+ timeout: this.timeoutMs
1426
+ });
1427
+ await this.page.locator(OPERATOR_LOGIN_USERNAME_SELECTOR).fill(username);
1428
+ }
1429
+ async enterPassword(password) {
1430
+ await this.page.waitForSelector?.(OPERATOR_LOGIN_GATE_SELECTOR, {
1431
+ timeout: this.timeoutMs
1432
+ });
1433
+ await this.page.locator(OPERATOR_LOGIN_PASSWORD_SELECTOR).fill(password);
1434
+ }
1435
+ async clickConfirm() {
1436
+ await this.page.locator(OPERATOR_LOGIN_CONFIRM_SELECTOR).click();
1437
+ const deadline = Date.now() + this.timeoutMs;
1438
+ while (Date.now() < deadline) {
1439
+ const credentialsStatus = await peekAttribute(
1440
+ this.page,
1441
+ CREDENTIALS_STATUS_SELECTOR,
1442
+ "data-credentials-status"
1443
+ );
1444
+ if (credentialsStatus === "INCORRECT_CREDENTIALS") {
1445
+ return { status: "INCORRECT_CREDENTIALS" };
1446
+ }
1447
+ if (await isAttached(
1448
+ this.page,
1449
+ ZERODHA_LOGIN_PAGE_SELECTOR,
1450
+ "data-zerodha-login-page"
1451
+ )) {
1452
+ return {
1453
+ status: "SUCCESS",
1454
+ zerodhaLoginPage: new PlaywrightZerodhaLoginPage(
1455
+ this.page,
1456
+ this.timeoutMs,
1457
+ this.interactiveLoginTimeoutMs,
1458
+ this.headless,
1459
+ this.registerCleanup
1460
+ )
1461
+ };
1462
+ }
1463
+ await sleep(50);
1464
+ }
1465
+ throw new Error(
1466
+ "Timed out waiting for the Zerodha Login Page after Operator Login confirm"
1467
+ );
1468
+ }
1469
+ };
1470
+ var PlaywrightCrawler = class {
1471
+ constructor(browser, page, baseUrl, timeoutMs, interactiveLoginTimeoutMs, headless) {
1472
+ this.browser = browser;
1473
+ this.page = page;
1474
+ this.baseUrl = baseUrl;
1475
+ this.timeoutMs = timeoutMs;
1476
+ this.interactiveLoginTimeoutMs = interactiveLoginTimeoutMs;
1477
+ this.headless = headless;
1478
+ }
1479
+ browser;
1480
+ page;
1481
+ baseUrl;
1482
+ timeoutMs;
1483
+ interactiveLoginTimeoutMs;
1484
+ headless;
1485
+ cleanups = /* @__PURE__ */ new Set();
1486
+ pageControls;
1487
+ async openHomePage() {
1488
+ await this.page.goto(this.baseUrl);
1489
+ return new PlaywrightHomePage(
1490
+ this.page,
1491
+ this.timeoutMs,
1492
+ this.interactiveLoginTimeoutMs,
1493
+ this.headless,
1494
+ (cleanup) => this.cleanups.add(cleanup)
1495
+ );
1496
+ }
1497
+ controls() {
1498
+ return this.pageControls ??= new PageControls(this.page, this.baseUrl);
1499
+ }
1500
+ getPage() {
1501
+ return this.page;
1502
+ }
1503
+ async close() {
1504
+ for (const cleanup of this.cleanups) {
1505
+ cleanup();
1506
+ }
1507
+ this.cleanups.clear();
1508
+ await this.browser.close();
1509
+ }
1510
+ async forceClose() {
1511
+ if (this.browser.forceClose) {
1512
+ await this.browser.forceClose();
1513
+ return;
1514
+ }
1515
+ await this.browser.close();
1516
+ }
1517
+ };
1518
+ async function createCrawler(options = {}) {
1519
+ const headless = options.headless ?? true;
1520
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1521
+ const launchTimeoutMs = options.launchTimeoutMs ?? DEFAULT_TIMEOUT_MS;
1522
+ const interactiveLoginTimeoutMs = options.interactiveLoginTimeoutMs ?? DEFAULT_INTERACTIVE_LOGIN_TIMEOUT_MS;
1523
+ const browser = await (options.launch ?? (() => chromium.launch({ headless, timeout: launchTimeoutMs })))();
1524
+ const page = await browser.newPage(options.contextOptions);
1525
+ page.setDefaultTimeout?.(timeoutMs);
1526
+ page.setDefaultNavigationTimeout?.(timeoutMs);
1527
+ return new PlaywrightCrawler(
1528
+ browser,
1529
+ page,
1530
+ options.baseUrl ?? DEFAULT_BASE_URL,
1531
+ timeoutMs,
1532
+ interactiveLoginTimeoutMs,
1533
+ headless
1534
+ );
1535
+ }
1536
+
1537
+ // ../cli/src/sms.ts
1538
+ import { spawn } from "node:child_process";
1539
+ var SmsReadError = class extends Error {
1540
+ constructor(code, message) {
1541
+ super(message);
1542
+ this.code = code;
1543
+ this.name = "SmsReadError";
1544
+ }
1545
+ code;
1546
+ };
1547
+ async function readSmsSecret(options) {
1548
+ if (!options.credential.trim()) {
1549
+ throw new SmsReadError("sms_credential_missing", "No protected SMS credential has been provisioned.");
1550
+ }
1551
+ if (!options.name.trim() || !options.scope.trim() || !options.endpoint.trim()) {
1552
+ throw new SmsReadError("sms_unavailable", "SMS configuration is incomplete.");
1553
+ }
1554
+ if (options.signal?.aborted) throw new SmsReadError("sms_unavailable", "SMS access was cancelled.");
1555
+ const command = options.command ?? "npx";
1556
+ const args = options.command ? ["read", options.name] : ["--yes", "--package=@ai-digital-factory/sms-cli@0.1.2", "sms", "read", options.name];
1557
+ return new Promise((resolve, reject) => {
1558
+ let child;
1559
+ try {
1560
+ child = spawn(command, args, {
1561
+ env: {
1562
+ ...process.env,
1563
+ SMS_ENDPOINT: options.endpoint,
1564
+ SMS_CREDENTIAL: options.credential,
1565
+ SMS_SCOPE: options.scope
1566
+ },
1567
+ stdio: ["ignore", "pipe", "ignore"]
1568
+ });
1569
+ } catch {
1570
+ reject(new SmsReadError("sms_unavailable", "SMS access is unavailable."));
1571
+ return;
1572
+ }
1573
+ const chunks = [];
1574
+ let length = 0;
1575
+ let settled = false;
1576
+ const finish = (error, result) => {
1577
+ if (settled) return;
1578
+ settled = true;
1579
+ clearTimeout(timer);
1580
+ options.signal?.removeEventListener("abort", onAbort);
1581
+ if (error) reject(error);
1582
+ else resolve(result ?? "");
1583
+ };
1584
+ const onAbort = () => {
1585
+ child.kill("SIGTERM");
1586
+ finish(new SmsReadError("sms_unavailable", "SMS access exceeded the action deadline."));
1587
+ };
1588
+ const timer = setTimeout(() => {
1589
+ child.kill("SIGTERM");
1590
+ finish(new SmsReadError("sms_unavailable", "SMS access exceeded the action deadline."));
1591
+ }, options.timeoutMs ?? 3e4);
1592
+ options.signal?.addEventListener("abort", onAbort, { once: true });
1593
+ child.stdout.on("data", (chunk) => {
1594
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1595
+ length += buffer.length;
1596
+ if (length > 1048576) {
1597
+ child.kill("SIGTERM");
1598
+ finish(new SmsReadError("sms_unavailable", "SMS returned an oversized secret record."));
1599
+ return;
1600
+ }
1601
+ chunks.push(buffer);
1602
+ });
1603
+ child.on("error", () => finish(new SmsReadError("sms_unavailable", "SMS access is unavailable.")));
1604
+ child.on("close", (code) => {
1605
+ if (code !== 0) {
1606
+ finish(new SmsReadError("sms_unavailable", "SMS access is unavailable for the selected Gate scope."));
1607
+ return;
1608
+ }
1609
+ const value = Buffer.concat(chunks).toString("utf8").trimEnd();
1610
+ if (!value.trim()) {
1611
+ finish(new SmsReadError("sms_unavailable", "SMS returned an empty secret record."));
1612
+ return;
1613
+ }
1614
+ finish(void 0, value);
1615
+ });
1616
+ });
1617
+ }
1618
+ async function readOperatorLoginCredentials(input) {
1619
+ let raw;
1620
+ try {
1621
+ raw = await (input.secretReader ?? readSmsSecret)({
1622
+ name: "live-market-terminal-operator-directory",
1623
+ scope: input.gateScope,
1624
+ endpoint: input.smsEndpoint,
1625
+ credential: input.smsCredential,
1626
+ ...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
1627
+ ...input.signal ? { signal: input.signal } : {},
1628
+ ...input.command ? { command: input.command } : {}
1629
+ });
1630
+ } catch (error) {
1631
+ if (error instanceof SmsReadError) throw error;
1632
+ throw new SmsReadError("sms_unavailable", "Operator Directory is unavailable from the selected Gate scope.");
1633
+ }
1634
+ try {
1635
+ const parsed = JSON.parse(raw);
1636
+ if (!parsed || typeof parsed !== "object" || !("operators" in parsed) || !Array.isArray(parsed.operators)) {
1637
+ throw new Error();
1638
+ }
1639
+ const matches = parsed.operators.filter(
1640
+ (entry2) => entry2 && typeof entry2 === "object" && "operatorId" in entry2 && entry2.operatorId === input.operatorId
1641
+ );
1642
+ if (matches.length !== 1) throw new Error();
1643
+ const entry = matches[0];
1644
+ if (typeof entry.loginUsername !== "string" || !entry.loginUsername.trim() || typeof entry.loginPassword !== "string" || !entry.loginPassword.trim()) throw new Error();
1645
+ return { username: entry.loginUsername.trim(), password: entry.loginPassword };
1646
+ } catch {
1647
+ throw new SmsReadError("invalid_operator_directory", "The selected Gate Operator Directory is invalid or does not contain exactly one configured Operator.");
1648
+ }
1649
+ }
1650
+
1651
+ // src/operator.ts
1652
+ var OperatorActionError = class extends Error {
1653
+ constructor(code, message) {
1654
+ super(message);
1655
+ this.code = code;
1656
+ }
1657
+ code;
1658
+ };
1659
+ async function operatorLogin(input) {
1660
+ const deadlineAt = Date.now() + input.timeoutMs;
1661
+ const remaining = () => {
1662
+ const timeout = deadlineAt - Date.now();
1663
+ if (timeout < 1) throw new Error("Operator login exceeded its deadline before submission.");
1664
+ return timeout;
1665
+ };
1666
+ let target;
1667
+ try {
1668
+ target = resolveCliTarget({ LMT_ENV: input.environment }, process.env.LMT_WEB_CONFIG_PATH || DEFAULT_CONFIG_PATH2);
1669
+ if (target.origin !== input.origin) throw new Error("The configured Web URL changed. Start a new browser session.");
1670
+ } catch {
1671
+ throw new OperatorActionError("invalid_sms_configuration", "Check smsEndpoint, Gate scope, and OPERATOR_1 in the selected Web configuration, then start a fresh session.");
1672
+ }
1673
+ let credential;
1674
+ try {
1675
+ credential = await new CliProfileStore(void 0, void 0, ".lmt-web").getSmsCredential();
1676
+ } catch {
1677
+ throw new OperatorActionError("sms_credential_unavailable", "Could not read protected SMS storage. Provision the SMS read credential again.");
1678
+ }
1679
+ if (!credential) throw new OperatorActionError("sms_credential_missing", `Provision the SMS read credential with \`lmt-web credential provision --env ${input.environment} --credential-stdin\` before Operator login.`);
1680
+ input.registerSecrets(credential);
1681
+ let credentials;
1682
+ try {
1683
+ credentials = await readOperatorLoginCredentials({
1684
+ operatorId: target.operatorId,
1685
+ gateScope: target.gateScope,
1686
+ smsEndpoint: target.smsEndpoint,
1687
+ smsCredential: credential,
1688
+ timeoutMs: remaining(),
1689
+ ...process.env.LMT_WEB_SMS_COMMAND ? { command: process.env.LMT_WEB_SMS_COMMAND } : {}
1690
+ });
1691
+ } catch (error) {
1692
+ throw new OperatorActionError(error instanceof SmsReadError ? error.code : "sms_unavailable", "Could not load Operator credentials from the selected Gate SMS scope. Check provisioning and SMS reachability.");
1693
+ }
1694
+ input.registerSecrets(credentials.username, credentials.password);
1695
+ const home = new PlaywrightHomePage(input.page, remaining());
1696
+ await home.enterUsername(credentials.username);
1697
+ await home.enterPassword(credentials.password);
1698
+ remaining();
1699
+ input.onSubmit?.();
1700
+ const result = await home.clickConfirm();
1701
+ if (result.status !== "SUCCESS") throw new OperatorActionError("incorrect_credentials", "The Home Page rejected the configured Operator credentials. Check the selected Gate Operator Directory.");
1702
+ }
1703
+ async function operatorLogout(page, timeoutMs, onSubmit = () => void 0) {
1704
+ const isOperatorPage = await page.evaluate?.(() => !!document.querySelector('[data-operator-page="true"]'));
1705
+ if (isOperatorPage) {
1706
+ await new PlaywrightOperatorPage(page, timeoutMs).clickLogoutZerodha(onSubmit);
1707
+ }
1708
+ await new PlaywrightZerodhaLoginPage(page, timeoutMs).clickLogoutAccount(onSubmit);
1709
+ }
1710
+
1711
+ // src/session.ts
1712
+ var SOCKET_DIRECTORY = path4.join(os.tmpdir(), `lmt-web-${process.getuid?.() ?? "user"}`);
1713
+ var SESSION_ID_PATTERN = /^[a-f0-9]{32}$/;
1714
+ var OWNER_RESPONSE_GRACE_MS = 1e3;
1715
+ var OWNER_CLEANUP_GRACE_MS = 2e3;
1716
+ var IPHONE_13_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1";
1717
+ function createSessionId() {
1718
+ return randomBytes2(16).toString("hex");
1719
+ }
1720
+ function isSessionId(value) {
1721
+ return !!value && SESSION_ID_PATTERN.test(value);
1722
+ }
1723
+ async function ensureSessionDirectory() {
1724
+ if (process.platform === "win32") {
1725
+ throw new Error("lmt-web session sockets are supported on macOS and Linux.");
1726
+ }
1727
+ await mkdir2(SOCKET_DIRECTORY, { recursive: true, mode: 448 });
1728
+ await chmod2(SOCKET_DIRECTORY, 448);
1729
+ const directory = await lstat2(SOCKET_DIRECTORY);
1730
+ const uid = process.getuid?.();
1731
+ if (!directory.isDirectory() || uid !== void 0 && directory.uid !== uid) {
1732
+ throw new Error("The lmt-web session directory is not owned by the invoking user.");
1733
+ }
1734
+ }
1735
+ function socketPathFor(sessionId) {
1736
+ return path4.join(SOCKET_DIRECTORY, `${sessionId}.sock`);
1737
+ }
1738
+ function artifactsDirectory(sessionId) {
1739
+ return path4.join(SOCKET_DIRECTORY, "artifacts", sessionId);
1740
+ }
1741
+ function redactSecrets(value) {
1742
+ return value.replace(
1743
+ /(access[\s_-]*token|request[\s_-]*token|api[\s_-]*(?:secret|key|token)|password|credential|(?:one[-\s]?time[-\s]?password)|otp)(?:\s*[:=]\s*|\s*\n\s*)([^\s,;]+)/gi,
1744
+ "$1: [redacted]"
1745
+ ).replace(/https?:\/\/[^\s)'"<>]+/gi, (candidate) => safeUrl(candidate) ?? "[url]");
1746
+ }
1747
+ function safeUrl(value) {
1748
+ try {
1749
+ const url = new URL(value);
1750
+ if (!/^https?:$/.test(url.protocol) || url.origin === "null") return null;
1751
+ return `${url.origin}${url.pathname}`;
1752
+ } catch {
1753
+ return null;
1754
+ }
1755
+ }
1756
+ function failureCode(error) {
1757
+ if (error instanceof ControlError) return error.code;
1758
+ const message = error instanceof Error ? error.message : "";
1759
+ if (/timed out|timeout|deadline/i.test(message)) return "deadline_exceeded";
1760
+ if (/executable doesn't exist|browser.*not.*installed|could not find.*browser/i.test(message)) {
1761
+ return "browser_unavailable";
1762
+ }
1763
+ if (/net::ERR_(?:CONNECTION_REFUSED|CONNECTION_TIMED_OUT|NAME_NOT_RESOLVED|ADDRESS_UNREACHABLE|INTERNET_DISCONNECTED|EMPTY_RESPONSE)/i.test(message)) {
1764
+ return "service_unavailable";
1765
+ }
1766
+ return "action_failed";
1767
+ }
1768
+ function messageFor(error) {
1769
+ const message = error instanceof Error ? error.message : "The browser action failed.";
1770
+ return redactSecrets(message).slice(0, 500);
1771
+ }
1772
+ function deviceContext(mode) {
1773
+ if (mode === "mobile") {
1774
+ return {
1775
+ name: "iPhone 13",
1776
+ contextOptions: {
1777
+ viewport: { width: 390, height: 844 },
1778
+ deviceScaleFactor: 3,
1779
+ isMobile: true,
1780
+ hasTouch: true,
1781
+ userAgent: IPHONE_13_USER_AGENT
1782
+ }
1783
+ };
1784
+ }
1785
+ return {
1786
+ name: "Desktop",
1787
+ contextOptions: {
1788
+ viewport: { width: 1440, height: 900 },
1789
+ deviceScaleFactor: 1,
1790
+ isMobile: false,
1791
+ hasTouch: false
1792
+ }
1793
+ };
1794
+ }
1795
+ async function launchSessionBrowser(headless, deadlineAt, onServerReady, onBrowserClosed) {
1796
+ const browserServer = await chromium2.launchServer({
1797
+ headless,
1798
+ timeout: Math.max(1, deadlineAt - Date.now())
1799
+ });
1800
+ let forceClosePromise;
1801
+ const forceClose = () => {
1802
+ forceClosePromise ??= browserServer.kill();
1803
+ return forceClosePromise;
1804
+ };
1805
+ onServerReady(forceClose);
1806
+ try {
1807
+ const connectTimeoutMs = Math.max(1, deadlineAt - Date.now());
1808
+ if (deadlineAt <= Date.now()) throw new Error("The browser action exceeded its deadline.");
1809
+ const browser = await chromium2.connect(browserServer.wsEndpoint(), { timeout: connectTimeoutMs });
1810
+ browser.once("disconnected", onBrowserClosed);
1811
+ let closePromise;
1812
+ return {
1813
+ newPage: async (options) => {
1814
+ const page = await browser.newPage(options);
1815
+ page.once("close", onBrowserClosed);
1816
+ return page;
1817
+ },
1818
+ close: () => {
1819
+ closePromise ??= (async () => {
1820
+ let closeError;
1821
+ try {
1822
+ await browser.close();
1823
+ } catch (error) {
1824
+ closeError = error;
1825
+ }
1826
+ try {
1827
+ await browserServer.close();
1828
+ } catch (error) {
1829
+ closeError ??= error;
1830
+ }
1831
+ if (closeError) throw closeError;
1832
+ })();
1833
+ return closePromise;
1834
+ },
1835
+ forceClose
1836
+ };
1837
+ } catch (error) {
1838
+ await forceClose().catch(() => void 0);
1839
+ throw error;
1840
+ }
1841
+ }
1842
+ async function waitForRenderedPage(page) {
1843
+ if (!page.evaluate) throw new Error("The browser page does not support inspection.");
1844
+ let settled = false;
1845
+ while (true) {
1846
+ const ready = await page.evaluate(() => {
1847
+ const visible = (element) => {
1848
+ const rect = element.getBoundingClientRect();
1849
+ const style = getComputedStyle(element);
1850
+ return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
1851
+ };
1852
+ const loading = Array.from(document.querySelectorAll('[data-session-loading], [aria-busy="true"]')).some(visible);
1853
+ const animating = document.getAnimations().some(
1854
+ (animation) => (animation.pending || animation.playState === "running") && Number.isFinite(Number(animation.effect?.getComputedTiming().endTime))
1855
+ );
1856
+ const imagesPending = Array.from(document.images).some((image) => visible(image) && !image.complete);
1857
+ return document.readyState === "complete" && document.fonts.status === "loaded" && !!document.body && !loading && !animating && !imagesPending;
1858
+ });
1859
+ if (ready && settled) return;
1860
+ settled = ready;
1861
+ await wait(50);
1862
+ }
1863
+ }
1864
+ async function inspectPage(page, mode, crawler) {
1865
+ await waitForRenderedPage(page);
1866
+ if (!page.evaluate) throw new Error("The browser page does not support inspection.");
1867
+ const result = await page.evaluate(() => {
1868
+ const visible = (element) => {
1869
+ const rect = element.getBoundingClientRect();
1870
+ const style = getComputedStyle(element);
1871
+ return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
1872
+ };
1873
+ const controls2 = Array.from(
1874
+ document.querySelectorAll("button, a[href], input, select, textarea, [role=button]")
1875
+ ).filter(visible).slice(0, 100).map((element, index) => {
1876
+ const labels = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement ? Array.from(element.labels ?? []).map((item) => item.innerText).join(" ") : "";
1877
+ const label = element.getAttribute("aria-label") || element.getAttribute("title") || labels || (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement ? element.placeholder : "") || (element instanceof HTMLSelectElement ? element.selectedOptions[0]?.innerText : "") || element.textContent || "";
1878
+ const kind = element.getAttribute("role") || element.tagName.toLowerCase();
1879
+ return {
1880
+ reference: `control-${index + 1}`,
1881
+ kind,
1882
+ label: label.replace(/\s+/g, " ").trim().slice(0, 160)
1883
+ };
1884
+ });
1885
+ return {
1886
+ title: document.title,
1887
+ text: document.body?.innerText ?? "",
1888
+ controls: controls2,
1889
+ viewport: { width: window.innerWidth, height: window.innerHeight },
1890
+ deviceScaleFactor: window.devicePixelRatio,
1891
+ hasTouch: navigator.maxTouchPoints > 0,
1892
+ userAgent: navigator.userAgent
1893
+ };
1894
+ });
1895
+ const controls = crawler ? await crawler.controls().inspect() : result.controls;
1896
+ const redact = (value) => redactSecrets(crawler?.controls().redact(value) ?? value);
1897
+ const viewport = page.viewportSize?.() ?? result.viewport;
1898
+ return {
1899
+ url: safeUrl(redact(page.url())),
1900
+ visibleState: {
1901
+ title: redact(result.title).slice(0, 300),
1902
+ text: redact(result.text).slice(0, 4e3),
1903
+ controls: controls.map((control) => ({ ...control, label: redact(control.label) }))
1904
+ },
1905
+ device: {
1906
+ name: deviceContext(mode).name,
1907
+ mode,
1908
+ viewport,
1909
+ deviceScaleFactor: result.deviceScaleFactor,
1910
+ hasTouch: result.hasTouch,
1911
+ userAgent: redactSecrets(result.userAgent).slice(0, 300)
1912
+ }
1913
+ };
1914
+ }
1915
+ async function captureScreenshot(page, sessionId, action, fullPage, timeoutMs, onTimeout) {
1916
+ if (!page) return { outcome: "unavailable", reason: "No browser page was available." };
1917
+ if (!page.screenshot) return { outcome: "failed", error: "The browser does not support screenshots." };
1918
+ const directory = artifactsDirectory(sessionId);
1919
+ const filename = `${Date.now()}-${action}-${randomBytes2(4).toString("hex")}.png`;
1920
+ const screenshotPath = path4.resolve(directory, filename);
1921
+ if (timeoutMs < 1) {
1922
+ return { outcome: "failed", error: "The action deadline elapsed before screenshot capture." };
1923
+ }
1924
+ try {
1925
+ await mkdir2(directory, { recursive: true, mode: 448 });
1926
+ await chmod2(directory, 448);
1927
+ await withDeadline(
1928
+ (async () => {
1929
+ await waitForRenderedPage(page);
1930
+ return page.screenshot({
1931
+ path: screenshotPath,
1932
+ fullPage,
1933
+ animations: "disabled",
1934
+ timeout: timeoutMs
1935
+ });
1936
+ })(),
1937
+ timeoutMs,
1938
+ onTimeout
1939
+ );
1940
+ await chmod2(screenshotPath, 384);
1941
+ return { outcome: "captured", path: screenshotPath, fullPage };
1942
+ } catch (error) {
1943
+ await unlink2(screenshotPath).catch(() => void 0);
1944
+ return { outcome: "failed", error: messageFor(error) };
1945
+ }
1946
+ }
1947
+ function remainingMs(deadlineAt) {
1948
+ return Math.max(0, deadlineAt - Date.now());
1949
+ }
1950
+ function withDeadline(promise, timeoutMs, onTimeout) {
1951
+ return new Promise((resolve, reject) => {
1952
+ let settled = false;
1953
+ const finish = (callback) => {
1954
+ if (settled) return;
1955
+ settled = true;
1956
+ clearTimeout(timeout);
1957
+ callback();
1958
+ };
1959
+ const timeout = setTimeout(() => {
1960
+ onTimeout?.();
1961
+ finish(() => reject(new Error("The browser action exceeded its deadline.")));
1962
+ }, Math.max(1, timeoutMs));
1963
+ promise.then(
1964
+ (value) => finish(() => resolve(value)),
1965
+ (error) => finish(() => reject(error))
1966
+ );
1967
+ });
1968
+ }
1969
+ async function runSessionOwner(options) {
1970
+ await ensureSessionDirectory();
1971
+ const socketPath = socketPathFor(options.sessionId);
1972
+ const device = deviceContext(options.mode);
1973
+ let crawler;
1974
+ let page;
1975
+ let startupError;
1976
+ let startComplete = false;
1977
+ let closing = false;
1978
+ let abortOnShutdown = false;
1979
+ let operationQueue = Promise.resolve();
1980
+ let shutdownPromise;
1981
+ let initializationPromise;
1982
+ let crawlerClosePromise;
1983
+ let expirationCleanup;
1984
+ let forceClosePendingBrowser;
1985
+ let pendingBrowserForceClosePromise;
1986
+ const sockets = /* @__PURE__ */ new Set();
1987
+ function closeCrawler() {
1988
+ if (!crawler) return Promise.resolve();
1989
+ crawlerClosePromise ??= crawler.close();
1990
+ return crawlerClosePromise;
1991
+ }
1992
+ function forceCloseBrowser() {
1993
+ if (crawler?.forceClose) return crawler.forceClose();
1994
+ if (!forceClosePendingBrowser) return Promise.resolve();
1995
+ pendingBrowserForceClosePromise ??= forceClosePendingBrowser();
1996
+ return pendingBrowserForceClosePromise;
1997
+ }
1998
+ function expireSession() {
1999
+ closing = true;
2000
+ abortOnShutdown = true;
2001
+ expirationCleanup ??= (async () => {
2002
+ if (initializationPromise) {
2003
+ await Promise.race([
2004
+ initializationPromise,
2005
+ wait(OWNER_CLEANUP_GRACE_MS)
2006
+ ]).catch(() => void 0);
2007
+ }
2008
+ if (crawler) await closeCrawler();
2009
+ else await forceCloseBrowser();
2010
+ })();
2011
+ void expirationCleanup.catch(() => void 0);
2012
+ }
2013
+ const server = net.createServer((socket) => {
2014
+ sockets.add(socket);
2015
+ socket.once("close", () => sockets.delete(socket));
2016
+ let payload = "";
2017
+ let handled = false;
2018
+ socket.on("data", (chunk) => {
2019
+ if (handled) return;
2020
+ payload += chunk.toString("utf8");
2021
+ const newline = payload.indexOf("\n");
2022
+ if (newline < 0) return;
2023
+ handled = true;
2024
+ const line = payload.slice(0, newline);
2025
+ let request;
2026
+ try {
2027
+ request = JSON.parse(line);
2028
+ } catch {
2029
+ socket.end(`${JSON.stringify(failureResult("inspect", "invalid_request", "The session request was invalid.", options.sessionId))}
2030
+ `);
2031
+ return;
2032
+ }
2033
+ const run = async () => {
2034
+ const response = await handleRequest(request);
2035
+ socket.end(`${JSON.stringify(response)}
2036
+ `);
2037
+ if (closing) socket.once("close", () => void shutdown());
2038
+ };
2039
+ operationQueue = operationQueue.then(run, run).catch(() => void 0);
2040
+ });
2041
+ });
2042
+ async function initialize(deadlineAt) {
2043
+ try {
2044
+ crawler = await createCrawler({
2045
+ baseUrl: options.origin,
2046
+ headless: options.headless,
2047
+ timeoutMs: Math.min(options.timeoutMs, Math.max(1, remainingMs(deadlineAt))),
2048
+ contextOptions: device.contextOptions,
2049
+ launch: () => {
2050
+ const onServerReady = (forceClose) => {
2051
+ forceClosePendingBrowser = forceClose;
2052
+ if (abortOnShutdown) void forceCloseBrowser().catch(() => void 0);
2053
+ };
2054
+ return options.launch ? options.launch(deadlineAt, onServerReady) : launchSessionBrowser(options.headless, deadlineAt, onServerReady, () => {
2055
+ if (closing) return;
2056
+ closing = true;
2057
+ abortOnShutdown = true;
2058
+ void shutdown();
2059
+ });
2060
+ }
2061
+ });
2062
+ page = crawler.getPage();
2063
+ if (closing) {
2064
+ await closeCrawler();
2065
+ return;
2066
+ }
2067
+ await crawler.openHomePage();
2068
+ } catch (error) {
2069
+ startupError = error;
2070
+ }
2071
+ }
2072
+ async function handleRequest(request) {
2073
+ if (!request || request.sessionId !== options.sessionId || !isAction(request.action) || !Number.isFinite(request.timeoutMs) || request.timeoutMs < 1 || !Number.isFinite(request.deadlineAt)) {
2074
+ return failureResult("inspect", "invalid_request", "The session request was invalid.", options.sessionId);
2075
+ }
2076
+ const deadlineAt = request.deadlineAt;
2077
+ if (remainingMs(deadlineAt) === 0) {
2078
+ return failureResult(request.action, "deadline_exceeded", "The browser action exceeded its deadline.", options.sessionId);
2079
+ }
2080
+ if (request.action === "start" && startComplete) {
2081
+ return failureResult("start", "session_already_started", "This browser session has already started.", options.sessionId);
2082
+ }
2083
+ if (!startComplete && request.action !== "start") {
2084
+ return failureResult(request.action, "session_starting", "The browser session is not ready yet.", options.sessionId);
2085
+ }
2086
+ if (request.action === "stop") {
2087
+ closing = true;
2088
+ abortOnShutdown = true;
2089
+ try {
2090
+ crawlerClosePromise = forceCloseBrowser();
2091
+ await withDeadline(crawlerClosePromise, remainingMs(deadlineAt), expireSession);
2092
+ return {
2093
+ version: RESULT_VERSION,
2094
+ sessionId: options.sessionId,
2095
+ action: "stop",
2096
+ outcome: "success",
2097
+ url: null,
2098
+ visibleState: null,
2099
+ device: null,
2100
+ screenshot: { outcome: "unavailable", reason: "Stop terminates the browser without capturing a screenshot." }
2101
+ };
2102
+ } catch (error2) {
2103
+ return failureResult("stop", failureCode(error2), messageFor(error2), options.sessionId);
2104
+ }
2105
+ }
2106
+ if (!startComplete) {
2107
+ initializationPromise ??= initialize(deadlineAt);
2108
+ try {
2109
+ await withDeadline(initializationPromise, remainingMs(deadlineAt), expireSession);
2110
+ } catch (error2) {
2111
+ startupError = error2;
2112
+ }
2113
+ }
2114
+ if (request.action === "start") {
2115
+ startComplete = true;
2116
+ let inspectionError;
2117
+ const inspected2 = page && !startupError && remainingMs(deadlineAt) > 0 ? await withDeadline(inspectPage(page, options.mode, crawler), remainingMs(deadlineAt), expireSession).catch((error2) => {
2118
+ inspectionError = error2;
2119
+ return null;
2120
+ }) : null;
2121
+ const screenshot2 = startupError || inspectionError ? { outcome: "unavailable", reason: "The page did not finish loading and inspection." } : await captureScreenshot(
2122
+ page,
2123
+ options.sessionId,
2124
+ "start",
2125
+ false,
2126
+ remainingMs(deadlineAt),
2127
+ expireSession
2128
+ );
2129
+ const actionError2 = startupError ?? inspectionError;
2130
+ const result = {
2131
+ version: RESULT_VERSION,
2132
+ sessionId: options.sessionId,
2133
+ action: "start",
2134
+ outcome: actionError2 ? "failure" : "success",
2135
+ url: inspected2?.url ?? (startupError ? safeUrl(options.origin) : page ? safeUrl(crawler?.controls().redact(page.url()) ?? page.url()) : null),
2136
+ visibleState: inspected2?.visibleState ?? null,
2137
+ device: inspected2?.device ?? null,
2138
+ screenshot: screenshot2,
2139
+ ...startupError ? {
2140
+ error: {
2141
+ code: failureCode(startupError),
2142
+ message: page ? failureCode(startupError) === "service_unavailable" ? "Could not load the configured Web URL. Confirm that the selected service is running and reachable." : failureCode(startupError) === "deadline_exceeded" ? "Opening the Home Page exceeded its deadline. Start a fresh session and retry." : messageFor(startupError) : failureCode(startupError) === "browser_unavailable" ? "Could not launch the browser. Install its Chromium runtime with `lmt-web install-browser` and retry." : messageFor(startupError)
2143
+ }
2144
+ } : inspectionError ? {
2145
+ error: {
2146
+ code: failureCode(inspectionError),
2147
+ message: "The Home Page loaded but its visible state could not be inspected."
2148
+ }
2149
+ } : {}
2150
+ };
2151
+ if (startupError) {
2152
+ closing = true;
2153
+ }
2154
+ return result;
2155
+ }
2156
+ if (startupError || !crawler || !page) {
2157
+ return failureResult(request.action, "session_unavailable", "The browser session is no longer available. Start a new session.", options.sessionId);
2158
+ }
2159
+ let guidedError;
2160
+ let submitted = false;
2161
+ const actionDeadlineAt = deadlineAt - Math.min(1500, Math.floor(remainingMs(deadlineAt) / 4));
2162
+ let actionTimedOut = false;
2163
+ const actionTimeout = () => Math.max(1, remainingMs(actionDeadlineAt));
2164
+ const actionExpired = () => {
2165
+ actionTimedOut = true;
2166
+ };
2167
+ page.setDefaultTimeout?.(actionTimeout());
2168
+ page.setDefaultNavigationTimeout?.(actionTimeout());
2169
+ let loginAttempt;
2170
+ const onSubmit = () => {
2171
+ submitted = true;
2172
+ };
2173
+ const zerodha = new PlaywrightZerodhaLoginPage(page, actionTimeout());
2174
+ try {
2175
+ if (request.action === "operator-login") await withDeadline(operatorLogin({ page, environment: options.environment ?? "local", origin: options.origin, timeoutMs: actionTimeout(), onSubmit, registerSecrets: (...values) => crawler.controls().registerSecrets(...values) }), actionTimeout(), actionExpired);
2176
+ if (request.action === "operator-logout") await withDeadline(operatorLogout(page, actionTimeout(), onSubmit), actionTimeout(), actionExpired);
2177
+ if (request.action === "zerodha-login") loginAttempt = await withDeadline(zerodha.startLoginAttempt(onSubmit), actionTimeout(), actionExpired);
2178
+ if (request.action === "zerodha-otp") {
2179
+ if (!request.attemptId || !request.otp || !/^\d{6}$/.test(request.otp)) throw new OperatorActionError("invalid_otp", "Supply a six-digit OTP and the attempt ID returned by zerodha-login.");
2180
+ crawler.controls().registerSecrets(request.otp);
2181
+ const result = await withDeadline(zerodha.submitActiveOtp(request.attemptId, request.otp, onSubmit), actionTimeout(), actionExpired);
2182
+ if (result.status !== "SUCCESS") throw new OperatorActionError("otp_rejected", result.message);
2183
+ }
2184
+ if (request.action === "zerodha-logout") await withDeadline(new PlaywrightOperatorPage(page, actionTimeout()).clickLogoutZerodha(onSubmit), actionTimeout(), actionExpired);
2185
+ } catch (error2) {
2186
+ guidedError = error2;
2187
+ }
2188
+ let inspected = null;
2189
+ let actionError;
2190
+ try {
2191
+ if (isControlAction(request)) {
2192
+ const budget = Math.max(1, remainingMs(deadlineAt) - Math.min(2e3, request.timeoutMs / 4));
2193
+ await withDeadline(crawler.controls().act(request, budget, () => {
2194
+ submitted = true;
2195
+ }), budget, () => void 0);
2196
+ }
2197
+ } catch (error2) {
2198
+ actionError = request.action === "click" && submitted && failureCode(error2) === "deadline_exceeded" ? new ControlError("action_uncertain", "The click may have been submitted but its completion was not confirmed. Inspect before deciding whether to retry.") : error2;
2199
+ if (request.action === "click" && failureCode(actionError) === "action_uncertain") {
2200
+ await withDeadline(crawler.controls().stopLoading(), Math.min(500, remainingMs(deadlineAt))).catch(() => void 0);
2201
+ }
2202
+ }
2203
+ try {
2204
+ inspected = await withDeadline(inspectPage(page, options.mode, crawler), remainingMs(deadlineAt), expireSession);
2205
+ } catch (error2) {
2206
+ actionError ??= error2;
2207
+ }
2208
+ const screenshot = await captureScreenshot(
2209
+ page,
2210
+ options.sessionId,
2211
+ request.action,
2212
+ request.fullPage === true,
2213
+ remainingMs(deadlineAt),
2214
+ expireSession
2215
+ );
2216
+ let closeError;
2217
+ if (request.action === "close") {
2218
+ closing = true;
2219
+ try {
2220
+ await withDeadline(closeCrawler(), remainingMs(deadlineAt), expireSession);
2221
+ } catch (error2) {
2222
+ closeError = error2;
2223
+ if (!abortOnShutdown) {
2224
+ await crawler.forceClose?.().catch(() => void 0);
2225
+ }
2226
+ }
2227
+ }
2228
+ if (actionTimedOut) closing = true;
2229
+ const error = guidedError ?? actionError ?? closeError;
2230
+ return {
2231
+ version: RESULT_VERSION,
2232
+ sessionId: options.sessionId,
2233
+ action: request.action,
2234
+ outcome: error instanceof ControlError && error.code === "action_uncertain" ? "uncertain" : error ? submitted && !(error instanceof OperatorActionError) && !(error instanceof LoginAttemptError) ? "uncertain" : "failure" : "success",
2235
+ ...loginAttempt ? { loginAttempt: { attemptId: loginAttempt.attemptId, status: loginAttempt.status } } : {},
2236
+ url: inspected?.url ?? (page ? safeUrl(crawler?.controls().redact(page.url()) ?? page.url()) : null),
2237
+ visibleState: inspected?.visibleState ?? null,
2238
+ device: inspected?.device ?? null,
2239
+ screenshot,
2240
+ ...error ? { error: { code: error instanceof OperatorActionError || error instanceof LoginAttemptError ? error.code : failureCode(error), message: crawler.controls().redact(error instanceof OperatorActionError || error instanceof LoginAttemptError ? error.message : messageFor(error)) } } : {}
2241
+ };
2242
+ }
2243
+ function shutdown() {
2244
+ if (!closing) return Promise.resolve();
2245
+ if (shutdownPromise) return shutdownPromise;
2246
+ shutdownPromise = (async () => {
2247
+ for (const socket of sockets) socket.destroy();
2248
+ await unlink2(socketPath).catch(() => void 0);
2249
+ server.close();
2250
+ if (abortOnShutdown) {
2251
+ const cleanup = crawler ? closeCrawler() : forceClosePendingBrowser ? forceCloseBrowser() : expirationCleanup ?? Promise.resolve();
2252
+ let cleanupFinished = false;
2253
+ let cleanupFailed = false;
2254
+ const observedCleanup = cleanup.then(
2255
+ () => {
2256
+ cleanupFinished = true;
2257
+ },
2258
+ () => {
2259
+ cleanupFinished = true;
2260
+ cleanupFailed = true;
2261
+ }
2262
+ );
2263
+ await Promise.race([observedCleanup, wait(OWNER_CLEANUP_GRACE_MS)]);
2264
+ if (!cleanupFinished || cleanupFailed) {
2265
+ await forceCloseBrowser().catch(() => void 0);
2266
+ }
2267
+ (options.exit ?? process.exit)(0);
2268
+ return;
2269
+ }
2270
+ if (crawler) {
2271
+ try {
2272
+ await closeCrawler();
2273
+ } catch {
2274
+ await crawler.forceClose?.().catch(() => void 0);
2275
+ }
2276
+ } else if (forceClosePendingBrowser) {
2277
+ await forceCloseBrowser().catch(() => void 0);
2278
+ }
2279
+ process.removeListener("SIGTERM", onSignal);
2280
+ process.removeListener("SIGINT", onSignal);
2281
+ })();
2282
+ return shutdownPromise;
2283
+ }
2284
+ await new Promise((resolve, reject) => {
2285
+ server.once("error", reject);
2286
+ server.listen(socketPath, () => {
2287
+ server.removeListener("error", reject);
2288
+ void chmod2(socketPath, 384).then(resolve, reject);
2289
+ });
2290
+ });
2291
+ const onSignal = () => {
2292
+ closing = true;
2293
+ void shutdown();
2294
+ };
2295
+ process.once("SIGTERM", onSignal);
2296
+ process.once("SIGINT", onSignal);
2297
+ }
2298
+ async function wait(milliseconds) {
2299
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
2300
+ }
2301
+ function assertOwnedSocket(socketPath) {
2302
+ return lstat2(socketPath).then((entry) => {
2303
+ const uid = process.getuid?.();
2304
+ if (!entry.isSocket() || uid !== void 0 && entry.uid !== uid) {
2305
+ throw new Error("The lmt-web session socket is not owned by the invoking user.");
2306
+ }
2307
+ });
2308
+ }
2309
+ async function requestSession(request, options = {}) {
2310
+ if (!isSessionId(request.sessionId)) {
2311
+ return failureResult(request.action, "invalid_session_id", "A valid session ID is required.", request.sessionId || null);
2312
+ }
2313
+ const socketPath = socketPathFor(request.sessionId);
2314
+ const deadlineAt = Date.now() + request.timeoutMs;
2315
+ const wireRequest = { ...request, deadlineAt };
2316
+ if (options.waitForSocket) {
2317
+ let ready = false;
2318
+ while (Date.now() < deadlineAt) {
2319
+ try {
2320
+ await assertOwnedSocket(socketPath);
2321
+ ready = true;
2322
+ break;
2323
+ } catch {
2324
+ if (options.ownerAlive && !options.ownerAlive()) {
2325
+ return failureResult(request.action, "browser_start_failed", "The browser session owner exited before it became ready.", request.sessionId);
2326
+ }
2327
+ await wait(40);
2328
+ }
2329
+ }
2330
+ if (!ready) {
2331
+ return failureResult(request.action, "deadline_exceeded", "The browser session did not become ready before its deadline.", request.sessionId);
2332
+ }
2333
+ }
2334
+ try {
2335
+ await assertOwnedSocket(socketPath);
2336
+ } catch {
2337
+ return failureResult(request.action, "session_not_found", "The browser session is not live. Start a new session.", request.sessionId);
2338
+ }
2339
+ return new Promise((resolve) => {
2340
+ const socket = net.createConnection(socketPath);
2341
+ let received = "";
2342
+ let settled = false;
2343
+ const finish = (result) => {
2344
+ if (settled) return;
2345
+ settled = true;
2346
+ clearTimeout(timer);
2347
+ socket.destroy();
2348
+ resolve(result);
2349
+ };
2350
+ const timer = setTimeout(() => {
2351
+ finish(failureResult(request.action, "deadline_exceeded", "The browser action exceeded its deadline.", request.sessionId));
2352
+ }, Math.max(1, deadlineAt + OWNER_RESPONSE_GRACE_MS - Date.now()));
2353
+ socket.once("connect", () => socket.write(`${JSON.stringify(wireRequest)}
2354
+ `));
2355
+ socket.on("data", (chunk) => {
2356
+ received += chunk.toString("utf8");
2357
+ const newline = received.indexOf("\n");
2358
+ if (newline < 0) return;
2359
+ try {
2360
+ finish(JSON.parse(received.slice(0, newline)));
2361
+ } catch {
2362
+ finish(failureResult(request.action, "invalid_response", "The browser session returned an invalid result.", request.sessionId));
2363
+ }
2364
+ });
2365
+ socket.once("error", () => {
2366
+ finish(failureResult(request.action, "session_not_found", "The browser session is not live. Start a new session.", request.sessionId));
2367
+ });
2368
+ socket.once("close", () => {
2369
+ if (!settled) {
2370
+ finish(failureResult(request.action, "session_not_found", "The browser session closed before returning a result.", request.sessionId));
2371
+ }
2372
+ });
2373
+ });
2374
+ }
2375
+
2376
+ // src/index.ts
2377
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
2378
+ var MAX_TIMEOUT_MS = 10 * 6e4;
2379
+ var HELP = `lmt-web \u2014 browser interaction with Live Market Terminal Web
2380
+
2381
+ Usage:
2382
+ lmt-web install-browser [--with-deps] [--dry-run]
2383
+ lmt-web start --env local|dev [--device mobile|desktop] [--headed] [--timeout 30s]
2384
+ lmt-web inspect --session ID [--full-page] [--timeout 30s]
2385
+ lmt-web click --session ID --ref REF
2386
+ lmt-web fill --session ID --ref REF --value TEXT [--secret]
2387
+ lmt-web select --session ID --ref REF --value OPTION
2388
+ lmt-web scroll --session ID [--ref REF] [--x PIXELS] [--y PIXELS]
2389
+ lmt-web navigate --session ID --url PATH_OR_URL
2390
+ lmt-web zerodha-login --session ID [--timeout 30s]
2391
+ lmt-web zerodha-otp --session ID --attempt ID --stdin [--timeout 30s]
2392
+ lmt-web zerodha-logout --session ID [--timeout 30s]
2393
+ lmt-web operator-login --session ID [--timeout 30s]
2394
+ lmt-web operator-logout --session ID [--timeout 30s]
2395
+ lmt-web credential provision --env local|dev --credential-stdin
2396
+ lmt-web close --session ID [--full-page] [--timeout 30s]
2397
+ lmt-web stop --session ID [--timeout 30s]
2398
+
2399
+ Start opens the configured Home Page in a fresh browser session. Mobile emulation
2400
+ and headless mode are the defaults. Inspect and close require the returned session ID.
2401
+ Stop terminates the session's browser and owner without capturing a final screenshot.
2402
+ Each command writes one versioned JSON result to stdout; diagnostics go to stderr.
2403
+ `;
2404
+ function parseTimeout(value) {
2405
+ const match = /^(\d+)(ms|s|m)?$/i.exec(value);
2406
+ if (!match) throw new InvocationError("--timeout must be a positive duration such as 30s, 500ms, or 2m.");
2407
+ const number = Number(match[1]);
2408
+ const multiplier = match[2]?.toLowerCase() === "m" ? 6e4 : match[2]?.toLowerCase() === "s" ? 1e3 : 1;
2409
+ const timeoutMs = number * multiplier;
2410
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
2411
+ throw new InvocationError("--timeout must be between 1ms and 10m.");
2412
+ }
2413
+ return timeoutMs;
2414
+ }
2415
+ async function parseCommand(argv) {
2416
+ const valueOptions = /* @__PURE__ */ new Set(["--timeout", "--session", ...Object.values(ACTIONS).flatMap((definition2) => definition2.values)]);
2417
+ const flagOptions = /* @__PURE__ */ new Set(["--full-page", ...Object.values(ACTIONS).flatMap((definition2) => definition2.flags)]);
2418
+ let actionIndex = 0;
2419
+ while (argv[actionIndex]?.startsWith("--")) {
2420
+ const option = argv[actionIndex];
2421
+ if (flagOptions.has(option)) {
2422
+ actionIndex += 1;
2423
+ } else if (valueOptions.has(option)) {
2424
+ const value = argv[actionIndex + 1];
2425
+ if (value === void 0 || value.startsWith("--")) throw new InvocationError(`${option} requires a value.`);
2426
+ actionIndex += 2;
2427
+ } else {
2428
+ throw new InvocationError(`Unknown option: ${option}`);
2429
+ }
2430
+ }
2431
+ const action = argv[actionIndex];
2432
+ if (!isAction(action)) throw new InvocationError("Choose a supported browser command. Run `lmt-web --help` for usage.");
2433
+ const rest = [...argv.slice(0, actionIndex), ...argv.slice(actionIndex + 1)];
2434
+ const definition = ACTIONS[action];
2435
+ const values = /* @__PURE__ */ new Map();
2436
+ const flags = /* @__PURE__ */ new Set();
2437
+ const allowedFlags = /* @__PURE__ */ new Set([...action === "start" || action === "stop" ? [] : ["--full-page"], ...definition.flags]);
2438
+ const allowedValues = /* @__PURE__ */ new Set(["--timeout", ...action === "start" ? [] : ["--session"], ...definition.values]);
2439
+ for (let index = 0; index < rest.length; index += 1) {
2440
+ const option = rest[index];
2441
+ if (!option.startsWith("--")) throw new InvocationError("Unexpected positional argument.");
2442
+ if (allowedFlags.has(option)) {
2443
+ if (flags.has(option)) throw new InvocationError(`${option} may only be supplied once.`);
2444
+ flags.add(option);
2445
+ continue;
2446
+ }
2447
+ if (!allowedValues.has(option)) throw new InvocationError(`Unknown option: ${option}`);
2448
+ if (values.has(option)) throw new InvocationError(`${option} may only be supplied once.`);
2449
+ const value = rest[index + 1];
2450
+ if (value === void 0 || value.startsWith("--")) throw new InvocationError(`${option} requires a value.`);
2451
+ values.set(option, value);
2452
+ index += 1;
2453
+ }
2454
+ const timeoutMs = values.has("--timeout") ? parseTimeout(values.get("--timeout")) : DEFAULT_TIMEOUT_MS2;
2455
+ if (action === "start") {
2456
+ const mode = values.get("--device") ?? "mobile";
2457
+ if (mode !== "mobile" && mode !== "desktop") {
2458
+ throw new InvocationError("--device must be mobile or desktop.");
2459
+ }
2460
+ return {
2461
+ action,
2462
+ environment: values.get("--env") ?? "",
2463
+ mode,
2464
+ headless: !flags.has("--headed"),
2465
+ timeoutMs
2466
+ };
2467
+ }
2468
+ const sessionId = values.get("--session");
2469
+ if (!isSessionId(sessionId)) {
2470
+ throw new InvocationError("--session requires the 32-character session ID returned by start.", "invalid_session_id");
2471
+ }
2472
+ let payload;
2473
+ try {
2474
+ payload = await definition.build({ values, flags, readOtp: () => readOtp(timeoutMs) });
2475
+ } catch (error) {
2476
+ throw new InvocationError(error instanceof Error ? error.message : "The action input could not be read.", error instanceof InvocationError ? error.code : "action_failed", action);
2477
+ }
2478
+ if (payload.action === "start") throw new InvocationError("Start requires an environment.");
2479
+ return { ...payload, sessionId, fullPage: flags.has("--full-page"), timeoutMs };
2480
+ }
2481
+ function emit(result, diagnostic) {
2482
+ process.stdout.write(`${JSON.stringify(result)}
2483
+ `);
2484
+ if (diagnostic) process.stderr.write(`${diagnostic}
2485
+ `);
2486
+ }
2487
+ async function readOtp(timeoutMs) {
2488
+ let input = "";
2489
+ const timer = setTimeout(() => process.stdin.destroy(new Error("OTP input exceeded its deadline.")), timeoutMs);
2490
+ try {
2491
+ for await (const chunk of process.stdin) {
2492
+ input += chunk.toString();
2493
+ if (input.length > 64) throw new InvocationError("OTP must contain six digits.");
2494
+ }
2495
+ const otp = input.trim();
2496
+ if (!/^\d{6}$/.test(otp)) throw new InvocationError("OTP must contain six digits.");
2497
+ return otp;
2498
+ } finally {
2499
+ input = "";
2500
+ clearTimeout(timer);
2501
+ }
2502
+ }
2503
+ async function startSession(command) {
2504
+ let target;
2505
+ try {
2506
+ target = resolveWebTarget(command.environment);
2507
+ } catch (error) {
2508
+ throw new InvocationError(error instanceof Error ? error.message : "The selected target is invalid.", "invalid_target");
2509
+ }
2510
+ await ensureSessionDirectory();
2511
+ const sessionId = createSessionId();
2512
+ const scriptPath = path5.resolve(process.argv[1] || fileURLToPath3(import.meta.url));
2513
+ const child = spawn2(
2514
+ process.execPath,
2515
+ [
2516
+ scriptPath,
2517
+ "__owner",
2518
+ sessionId,
2519
+ target.origin,
2520
+ command.mode,
2521
+ command.headless ? "headless" : "headed",
2522
+ String(command.timeoutMs),
2523
+ target.environment
2524
+ ],
2525
+ {
2526
+ detached: true,
2527
+ stdio: "ignore"
2528
+ }
2529
+ );
2530
+ let ownerAlive = true;
2531
+ child.once("error", () => {
2532
+ ownerAlive = false;
2533
+ });
2534
+ child.once("exit", () => {
2535
+ ownerAlive = false;
2536
+ });
2537
+ child.unref();
2538
+ const result = await requestSession({ sessionId, action: "start", timeoutMs: command.timeoutMs }, {
2539
+ waitForSocket: true,
2540
+ ownerAlive: () => ownerAlive
2541
+ });
2542
+ if (result.outcome === "failure" && result.error?.code === "deadline_exceeded") {
2543
+ try {
2544
+ process.kill(child.pid, "SIGTERM");
2545
+ } catch {
2546
+ }
2547
+ }
2548
+ return result;
2549
+ }
2550
+ async function runOwner(argv) {
2551
+ const [sessionId, origin, modeValue, headlessValue, timeoutValue, environment] = argv;
2552
+ if (!isSessionId(sessionId) || !origin || modeValue !== "mobile" && modeValue !== "desktop" || headlessValue !== "headless" && headlessValue !== "headed" || !timeoutValue || !/^\d+$/.test(timeoutValue)) {
2553
+ process.exitCode = 2;
2554
+ return;
2555
+ }
2556
+ await runSessionOwner({
2557
+ sessionId,
2558
+ origin,
2559
+ environment: environment === "dev" ? "dev" : "local",
2560
+ mode: modeValue,
2561
+ headless: headlessValue === "headless",
2562
+ timeoutMs: Math.min(Number(timeoutValue), MAX_TIMEOUT_MS)
2563
+ });
2564
+ }
2565
+ async function installBrowser(options) {
2566
+ const allowed = /* @__PURE__ */ new Set(["--with-deps", "--dry-run"]);
2567
+ if (options.some((option) => !allowed.has(option)) || new Set(options).size !== options.length) {
2568
+ process.stdout.write(`${JSON.stringify({ version: 1, action: "install-browser", outcome: "failure", error: { code: "invalid_invocation", message: "Use install-browser [--with-deps] [--dry-run]." } })}
2569
+ `);
2570
+ process.exitCode = 2;
2571
+ return;
2572
+ }
2573
+ try {
2574
+ const require2 = createRequire(import.meta.url);
2575
+ const browserCli = path5.join(path5.dirname(require2.resolve("playwright/package.json")), "cli.js");
2576
+ const code = await new Promise((resolve, reject) => {
2577
+ const child = spawn2(process.execPath, [browserCli, "install", ...options, "chromium"], {
2578
+ stdio: ["ignore", "pipe", "pipe"]
2579
+ });
2580
+ child.stdout.pipe(process.stderr);
2581
+ child.stderr.pipe(process.stderr);
2582
+ child.once("error", reject);
2583
+ child.once("close", (status) => resolve(status ?? 1));
2584
+ });
2585
+ process.stdout.write(`${JSON.stringify({ version: 1, action: "install-browser", outcome: code === 0 ? "success" : "failure", ...code === 0 ? {} : { error: { code: "browser_install_failed", message: "Chromium setup failed. Review stderr and retry install-browser." } } })}
2586
+ `);
2587
+ process.exitCode = code;
2588
+ } catch {
2589
+ process.stdout.write(`${JSON.stringify({ version: 1, action: "install-browser", outcome: "failure", error: { code: "browser_install_failed", message: "Could not run the installed Playwright runtime. Reinstall lmt-web and retry." } })}
2590
+ `);
2591
+ process.exitCode = 1;
2592
+ }
2593
+ }
2594
+ async function runCli(argv) {
2595
+ if (argv.includes("--help") || argv.includes("-h")) {
2596
+ process.stdout.write(HELP);
2597
+ return;
2598
+ }
2599
+ if (argv[0] === "install-browser" && !argv.includes("--help") && !argv.includes("-h")) {
2600
+ await installBrowser(argv.slice(1));
2601
+ return;
2602
+ }
2603
+ if (argv[0] === "credential" && argv[1] === "provision") {
2604
+ let credential = "";
2605
+ const timer = setTimeout(() => process.stdin.destroy(new Error("Credential input exceeded its deadline.")), DEFAULT_TIMEOUT_MS2);
2606
+ try {
2607
+ const values = /* @__PURE__ */ new Map();
2608
+ let credentialStdin = false;
2609
+ const legacyStdin = argv.length === 3 && argv[2] === "--stdin";
2610
+ if (!legacyStdin) {
2611
+ for (let index = 2; index < argv.length; index += 1) {
2612
+ const option = argv[index];
2613
+ if (option === "--credential-stdin") {
2614
+ if (credentialStdin) throw new InvocationError("--credential-stdin may only be supplied once.");
2615
+ credentialStdin = true;
2616
+ } else if (option === "--env") {
2617
+ const value = argv[index + 1];
2618
+ if (values.has(option) || value === void 0 || value.startsWith("--")) throw new InvocationError(`${option} requires exactly one value.`);
2619
+ values.set(option, value);
2620
+ index += 1;
2621
+ } else throw new InvocationError("Use credential provision --env local|dev --credential-stdin. Web CLI does not support profiles.");
2622
+ }
2623
+ if (!credentialStdin || !values.get("--env")) throw new InvocationError("Credential provision requires --env local|dev and --credential-stdin.");
2624
+ try {
2625
+ resolveCliTarget({ LMT_ENV: values.get("--env") }, process.env.LMT_WEB_CONFIG_PATH || DEFAULT_CONFIG_PATH2);
2626
+ } catch {
2627
+ throw new InvocationError("Choose a configured local or dev environment.", "invalid_target");
2628
+ }
2629
+ }
2630
+ if (process.stdin.isTTY) throw new InvocationError("Pipe the SMS credential on standard input.");
2631
+ for await (const chunk of process.stdin) {
2632
+ credential += chunk.toString();
2633
+ if (credential.length > 16384) throw new Error("SMS credential input is too long.");
2634
+ }
2635
+ await new CliProfileStore(void 0, void 0, ".lmt-web").saveSmsCredential(credential.replace(/\r?\n$/, ""));
2636
+ process.stdout.write(`${JSON.stringify({ version: 1, action: "credential-provision", outcome: "success", ...legacyStdin ? {} : { environment: values.get("--env") } })}
2637
+ `);
2638
+ } catch (error) {
2639
+ const code = error instanceof InvocationError ? error.code : "credential_provision_failed";
2640
+ const message = error instanceof InvocationError ? error.message : "Could not save the SMS read credential. Supply one non-empty line on stdin and check protected storage permissions.";
2641
+ process.stdout.write(`${JSON.stringify({ version: 1, action: "credential-provision", outcome: "failure", error: { code, message } })}
2642
+ `);
2643
+ process.stderr.write(`${code}: ${message}
2644
+ `);
2645
+ process.exitCode = error instanceof InvocationError ? 2 : 1;
2646
+ } finally {
2647
+ credential = "";
2648
+ clearTimeout(timer);
2649
+ }
2650
+ return;
2651
+ }
2652
+ if (argv[0] === "__owner") {
2653
+ await runOwner(argv.slice(1));
2654
+ return;
2655
+ }
2656
+ let command;
2657
+ try {
2658
+ command = await parseCommand(argv);
2659
+ let result;
2660
+ if (command.action === "start") {
2661
+ result = await startSession(command);
2662
+ } else {
2663
+ result = await requestSession(command);
2664
+ }
2665
+ emit(result, result.error ? `${result.error.code}: ${result.error.message}` : void 0);
2666
+ if (result.outcome !== "success") {
2667
+ process.exitCode = result.error?.code === "invalid_target" || result.error?.code === "invalid_invocation" ? 2 : 1;
2668
+ }
2669
+ } catch (error) {
2670
+ const action = command?.action ?? (error instanceof InvocationError ? error.action : void 0) ?? (argv[0] === "inspect" || argv[0] === "close" ? argv[0] : "start");
2671
+ const code = error instanceof InvocationError ? error.code : "action_failed";
2672
+ const message = error instanceof Error ? error.message : "The lmt-web command failed.";
2673
+ const result = failureResult(action, code, message);
2674
+ emit(result, `${code}: ${message}`);
2675
+ process.exitCode = error instanceof InvocationError && error.code !== "action_failed" ? 2 : 1;
2676
+ return;
2677
+ }
2678
+ }
2679
+ var arguments_ = process.argv.slice(2);
2680
+ void runCli(arguments_).then(() => {
2681
+ }).catch((error) => {
2682
+ const message = error instanceof Error ? error.message : "The lmt-web owner failed.";
2683
+ process.stderr.write(`${message}
2684
+ `);
2685
+ process.exitCode = 1;
2686
+ });