@ai-digital-factory/lmt-cli 0.1.6 → 1.0.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.
@@ -0,0 +1,1483 @@
1
+ // src/config.ts
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ var repoRoot = path.resolve(
6
+ path.dirname(fileURLToPath(import.meta.url)),
7
+ "../../../../"
8
+ );
9
+ var packagedConfigPath = fileURLToPath(new URL("../lmt.config.json", import.meta.url));
10
+ var DEFAULT_CONFIG_PATH = existsSync(packagedConfigPath) ? packagedConfigPath : path.join(repoRoot, "lmt.config.json");
11
+ function resolveCanonicalOrigin(value, environment) {
12
+ if (typeof value !== "string" || !value.trim()) {
13
+ throw new Error(`lmt.config.json does not contain the ${environment} Origin.`);
14
+ }
15
+ let origin;
16
+ try {
17
+ origin = new URL(value.trim());
18
+ } catch {
19
+ throw new Error(`${environment} Origin must be an absolute HTTP URL.`);
20
+ }
21
+ if (!/^https?:$/.test(origin.protocol) || origin.username || origin.password || origin.hash) {
22
+ throw new Error(
23
+ `${environment} Origin must be an HTTP URL without credentials or a fragment.`
24
+ );
25
+ }
26
+ if (origin.pathname !== "/" || origin.search) {
27
+ throw new Error(`${environment} Origin must not include a path or query string.`);
28
+ }
29
+ return origin.origin;
30
+ }
31
+ var LOCAL_CONFIG_FILE = "lmt.config.local.json";
32
+ var LocalConfigError = class extends Error {
33
+ code = "invalid_local_config";
34
+ };
35
+ function findCheckoutRoot(workingDirectory) {
36
+ let directory = path.resolve(workingDirectory);
37
+ for (; ; ) {
38
+ if (existsSync(path.join(directory, ".git"))) return directory;
39
+ const parent = path.dirname(directory);
40
+ if (parent === directory) return void 0;
41
+ directory = parent;
42
+ }
43
+ }
44
+ function checkoutLocalOrigin(workingDirectory) {
45
+ const root = findCheckoutRoot(workingDirectory);
46
+ if (!root) return void 0;
47
+ const file = path.join(root, LOCAL_CONFIG_FILE);
48
+ if (!existsSync(file)) return void 0;
49
+ let config;
50
+ try {
51
+ config = JSON.parse(readFileSync(file, "utf8"));
52
+ } catch {
53
+ throw new LocalConfigError(`Could not read ${file} as JSON.`);
54
+ }
55
+ const value = config?.environments?.local?.origin;
56
+ if (value === void 0) return void 0;
57
+ try {
58
+ return resolveCanonicalOrigin(value, "local");
59
+ } catch (error) {
60
+ throw new LocalConfigError(`${file}: ${error instanceof Error ? error.message : "invalid local Origin."}`);
61
+ }
62
+ }
63
+ function resolveTargetOrigin(environment, canonicalOrigin, workingDirectory = process.cwd()) {
64
+ const origin = resolveCanonicalOrigin(canonicalOrigin, environment);
65
+ return environment === "local" && checkoutLocalOrigin(workingDirectory) || origin;
66
+ }
67
+ function resolveCliTarget(env = process.env, configPath = DEFAULT_CONFIG_PATH, workingDirectory = process.cwd()) {
68
+ const environment = env.LMT_ENV?.trim();
69
+ if (environment !== "local" && environment !== "dev") {
70
+ if (environment === "prod") {
71
+ throw new Error("The CLI supports local or dev targets only; prod is not permitted.");
72
+ }
73
+ throw new Error("Target must explicitly select --env local or --env dev; prod is not permitted.");
74
+ }
75
+ let config;
76
+ try {
77
+ config = JSON.parse(readFileSync(configPath, "utf8"));
78
+ } catch {
79
+ throw new Error("Could not read the deployment configuration lmt.config.json.");
80
+ }
81
+ const selected = config.environments?.[environment];
82
+ if (!selected) {
83
+ throw new Error(`lmt.config.json does not contain the ${environment} environment.`);
84
+ }
85
+ const origin = resolveTargetOrigin(environment, selected.origin, workingDirectory);
86
+ const smsEndpoint = typeof config.smsEndpoint === "string" ? config.smsEndpoint.trim() : "";
87
+ if (!smsEndpoint) throw new Error("lmt.config.json does not contain smsEndpoint.");
88
+ const gateScope = typeof selected.gateScope === "string" ? selected.gateScope.trim() : "";
89
+ if (!gateScope) throw new Error(`lmt.config.json does not contain the ${environment} Gate scope.`);
90
+ const operators = selected.brokerOperatorIds;
91
+ if (!Array.isArray(operators) || operators.length !== 1 || operators[0] !== "OPERATOR_1") {
92
+ throw new Error(`${environment} must have exactly one configured Operator: OPERATOR_1.`);
93
+ }
94
+ let smsUrl;
95
+ try {
96
+ smsUrl = new URL(smsEndpoint);
97
+ } catch {
98
+ throw new Error("smsEndpoint must be an absolute HTTP URL.");
99
+ }
100
+ if (!/^https?:$/.test(smsUrl.protocol) || smsUrl.username || smsUrl.password || smsUrl.hash) {
101
+ throw new Error("smsEndpoint must be an HTTP URL without credentials or a fragment.");
102
+ }
103
+ return {
104
+ environment,
105
+ origin,
106
+ gateScope,
107
+ smsEndpoint: smsUrl.origin,
108
+ operatorId: "OPERATOR_1"
109
+ };
110
+ }
111
+ function resolveCliApiOrigin(target) {
112
+ return target.origin;
113
+ }
114
+
115
+ // src/profile-name.ts
116
+ var CLIENT_PROFILE_NAME_ERROR = "Profile must be 1\u201364 letters, digits, dots, underscores, or hyphens, starting with a letter or digit.";
117
+ var clientProfilePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
118
+ function assertClientProfileName(profile) {
119
+ if (!clientProfilePattern.test(profile)) throw new Error(CLIENT_PROFILE_NAME_ERROR);
120
+ }
121
+
122
+ // src/profile-store.ts
123
+ import { randomUUID } from "node:crypto";
124
+ import {
125
+ chmod,
126
+ constants,
127
+ lstat,
128
+ mkdir,
129
+ open,
130
+ rename,
131
+ unlink
132
+ } from "node:fs/promises";
133
+ import path2 from "node:path";
134
+ var CONTROLLER_SESSION = /* @__PURE__ */ Symbol("lmt.controller-session");
135
+ function ownerOf(profile) {
136
+ return profile ?? CONTROLLER_SESSION;
137
+ }
138
+ function profileNameOf(owner) {
139
+ return owner === CONTROLLER_SESSION ? void 0 : owner;
140
+ }
141
+ var FILE_NAME = "secrets.pem";
142
+ var OWNER_ONLY_DIRECTORY = 448;
143
+ var OWNER_ONLY_FILE = 384;
144
+ var LOCK_WAIT_MS = 5e3;
145
+ var LOCK_RETRY_MS = 20;
146
+ function emptyRecord() {
147
+ return { version: 2, profiles: {} };
148
+ }
149
+ function targetsEqual(left, right) {
150
+ return left.environment === right.environment && left.origin === right.origin && left.operatorId === right.operatorId;
151
+ }
152
+ function assertCookie(value, name) {
153
+ if (value !== void 0 && !new RegExp(`^${name}=[A-Za-z0-9._~-]+$`).test(value)) {
154
+ throw new Error(`The ${name} cookie is not valid.`);
155
+ }
156
+ }
157
+ function ownerId() {
158
+ return typeof process.getuid === "function" ? process.getuid() : void 0;
159
+ }
160
+ async function ensureOwnerOnly(pathname, kind) {
161
+ let info;
162
+ try {
163
+ info = await lstat(pathname);
164
+ } catch (error) {
165
+ if (error.code === "ENOENT") throw error;
166
+ throw new Error("Could not inspect the protected CLI profile storage.");
167
+ }
168
+ if (info.isSymbolicLink() || (kind === "directory" ? !info.isDirectory() : !info.isFile())) {
169
+ throw new Error(`The protected CLI profile ${kind} must be a regular ${kind}.`);
170
+ }
171
+ const uid = ownerId();
172
+ if (uid !== void 0 && info.uid !== uid) {
173
+ throw new Error(`The protected CLI profile ${kind} must be owned by the current user.`);
174
+ }
175
+ const expectedMode = kind === "directory" ? OWNER_ONLY_DIRECTORY : OWNER_ONLY_FILE;
176
+ if ((info.mode & 511) === expectedMode) return;
177
+ try {
178
+ await chmod(pathname, expectedMode);
179
+ } catch {
180
+ throw new Error(`Could not set protected CLI profile ${kind} permissions.`);
181
+ }
182
+ const updated = await lstat(pathname);
183
+ if ((updated.mode & 511) !== expectedMode) {
184
+ const mode = expectedMode.toString(8).padStart(4, "0");
185
+ throw new Error(`The protected CLI profile ${kind} must have mode ${mode}.`);
186
+ }
187
+ }
188
+ function parseLegacyTargetKey(key) {
189
+ try {
190
+ const parsed = JSON.parse(key);
191
+ if (!Array.isArray(parsed) || parsed.length !== 3) return void 0;
192
+ const [environment, origin, operatorId] = parsed;
193
+ if (environment !== "local" && environment !== "dev") return void 0;
194
+ if (typeof origin !== "string" || !origin.trim()) return void 0;
195
+ if (operatorId !== "OPERATOR_1") return void 0;
196
+ return { environment, origin: origin.trim(), operatorId };
197
+ } catch {
198
+ return void 0;
199
+ }
200
+ }
201
+ function migrateLegacyRecord(record) {
202
+ const migrated = emptyRecord();
203
+ if (record.smsCredential?.trim()) migrated.smsCredential = record.smsCredential;
204
+ for (const [profileName, profileRecord] of Object.entries(record.profiles ?? {})) {
205
+ const sessions = [];
206
+ for (const [targetKey, stored] of Object.entries(profileRecord.targets ?? {})) {
207
+ const target = parseLegacyTargetKey(targetKey);
208
+ if (!target) continue;
209
+ sessions.push({
210
+ ...target,
211
+ ...stored.operatorSessionCookie ? { operatorSessionCookie: stored.operatorSessionCookie } : {},
212
+ ...stored.clientIdentityCookie ? { clientIdentityCookie: stored.clientIdentityCookie } : {},
213
+ updatedAt: stored.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
214
+ });
215
+ }
216
+ if (sessions.length > 0) migrated.profiles[profileName] = { sessions };
217
+ }
218
+ return migrated;
219
+ }
220
+ function normalizeSession(value) {
221
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
222
+ const session = value;
223
+ if (session.environment !== "local" && session.environment !== "dev") return void 0;
224
+ if (typeof session.origin !== "string" || !session.origin.trim()) return void 0;
225
+ if (session.operatorId !== "OPERATOR_1") return void 0;
226
+ if (typeof session.updatedAt !== "string" || !session.updatedAt.trim()) return void 0;
227
+ if (session.operatorSessionCookie !== void 0 && typeof session.operatorSessionCookie !== "string") return void 0;
228
+ if (session.clientIdentityCookie !== void 0 && typeof session.clientIdentityCookie !== "string") return void 0;
229
+ return {
230
+ environment: session.environment,
231
+ origin: session.origin.trim(),
232
+ operatorId: session.operatorId,
233
+ updatedAt: session.updatedAt,
234
+ ...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
235
+ ...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {}
236
+ };
237
+ }
238
+ function normalizeControllerSession(value) {
239
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
240
+ const session = value;
241
+ if (session.environment !== "local" && session.environment !== "dev") return void 0;
242
+ if (session.operatorId !== "OPERATOR_1") return void 0;
243
+ if (typeof session.updatedAt !== "string" || !session.updatedAt.trim()) return void 0;
244
+ if (session.operatorSessionCookie !== void 0 && typeof session.operatorSessionCookie !== "string") return void 0;
245
+ if (session.clientIdentityCookie !== void 0 && typeof session.clientIdentityCookie !== "string") return void 0;
246
+ return {
247
+ environment: session.environment,
248
+ operatorId: session.operatorId,
249
+ updatedAt: session.updatedAt,
250
+ ...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
251
+ ...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {}
252
+ };
253
+ }
254
+ function parseRecord(raw) {
255
+ let parsed;
256
+ try {
257
+ parsed = JSON.parse(raw);
258
+ } catch {
259
+ throw new Error("The protected CLI profile file is not valid JSON.");
260
+ }
261
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
262
+ throw new Error("The protected CLI profile file has an unsupported format.");
263
+ }
264
+ const record = parsed;
265
+ if (record.version === 1) {
266
+ return migrateLegacyRecord(parsed);
267
+ }
268
+ if (record.version !== 2 || !record.profiles || typeof record.profiles !== "object" || Array.isArray(record.profiles)) {
269
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
270
+ }
271
+ if (record.smsCredential !== void 0 && (typeof record.smsCredential !== "string" || !record.smsCredential.trim())) {
272
+ throw new Error("The protected CLI profile file has an invalid SMS credential record.");
273
+ }
274
+ const profiles = {};
275
+ for (const [profileName, profileValue] of Object.entries(record.profiles)) {
276
+ if (!profileValue || typeof profileValue !== "object" || Array.isArray(profileValue)) {
277
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
278
+ }
279
+ const sessionsValue = profileValue.sessions;
280
+ if (!Array.isArray(sessionsValue)) {
281
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
282
+ }
283
+ const sessions = sessionsValue.map(normalizeSession).filter((session) => session !== void 0);
284
+ if (sessions.length !== sessionsValue.length) {
285
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
286
+ }
287
+ if (sessions.length > 0) profiles[profileName] = { sessions };
288
+ }
289
+ let controllerSessions = [];
290
+ if (record.controllerSessions !== void 0) {
291
+ if (!Array.isArray(record.controllerSessions)) {
292
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
293
+ }
294
+ controllerSessions = record.controllerSessions.map(normalizeControllerSession).filter((session) => session !== void 0);
295
+ if (controllerSessions.length !== record.controllerSessions.length) {
296
+ throw new Error("The protected CLI profile file has an unsupported version or format.");
297
+ }
298
+ }
299
+ return {
300
+ version: 2,
301
+ ...typeof record.smsCredential === "string" && record.smsCredential.trim() ? { smsCredential: record.smsCredential } : {},
302
+ ...controllerSessions.length > 0 ? { controllerSessions } : {},
303
+ profiles
304
+ };
305
+ }
306
+ function sessionMatches(owner, session, target) {
307
+ if (profileNameOf(owner) === void 0) return session.environment === target.environment && session.operatorId === target.operatorId;
308
+ return session.origin !== void 0 && targetsEqual({ ...session, origin: session.origin }, target);
309
+ }
310
+ function assertOwner(owner) {
311
+ const profile = profileNameOf(owner);
312
+ if (profile !== void 0) assertClientProfileName(profile);
313
+ }
314
+ function sessionsOf(record, owner) {
315
+ const profile = profileNameOf(owner);
316
+ return [...profile === void 0 ? record.controllerSessions ?? [] : record.profiles[profile]?.sessions ?? []];
317
+ }
318
+ function setSessions(record, owner, sessions) {
319
+ const profile = profileNameOf(owner);
320
+ if (profile === void 0) {
321
+ if (sessions.length === 0) delete record.controllerSessions;
322
+ else record.controllerSessions = sessions;
323
+ } else if (sessions.length === 0) {
324
+ delete record.profiles[profile];
325
+ } else {
326
+ record.profiles[profile] = { sessions };
327
+ }
328
+ }
329
+ function storedSession(owner, target, cookies) {
330
+ return {
331
+ environment: target.environment,
332
+ ...profileNameOf(owner) === void 0 ? {} : { origin: target.origin },
333
+ operatorId: target.operatorId,
334
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
335
+ ...cookies.clientIdentityCookie ? { clientIdentityCookie: cookies.clientIdentityCookie } : {},
336
+ ...cookies.operatorSessionCookie ? { operatorSessionCookie: cookies.operatorSessionCookie } : {}
337
+ };
338
+ }
339
+ function sessionHasCookies(session) {
340
+ return Boolean(session.operatorSessionCookie || session.clientIdentityCookie);
341
+ }
342
+ var CliProfileStore = class {
343
+ constructor(homeDirectory = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd(), lockWaitMs = LOCK_WAIT_MS) {
344
+ this.lockWaitMs = lockWaitMs;
345
+ this.directory = path2.join(homeDirectory, ".lmt");
346
+ this.filePath = path2.join(this.directory, FILE_NAME);
347
+ this.lockPath = path2.join(this.directory, `${FILE_NAME}.lock`);
348
+ }
349
+ lockWaitMs;
350
+ directory;
351
+ filePath;
352
+ lockPath;
353
+ async getSmsCredential() {
354
+ return this.withLock(async () => (await this.readRecord()).smsCredential);
355
+ }
356
+ async saveSmsCredential(credential) {
357
+ if (!credential.trim() || /[\r\n]/.test(credential)) throw new Error("SMS credential input must be a non-empty single line.");
358
+ await this.withLock(async () => {
359
+ const record = await this.readRecordForSmsCredentialUpdate();
360
+ record.smsCredential = credential;
361
+ await this.writeRecord(record);
362
+ });
363
+ }
364
+ async getSession(owner, target) {
365
+ assertOwner(owner);
366
+ return this.withLock(async () => {
367
+ const stored = sessionsOf(await this.readRecord(), owner).find((session) => sessionMatches(owner, session, target));
368
+ if (!stored) return void 0;
369
+ return {
370
+ ...stored.operatorSessionCookie ? { operatorSessionCookie: stored.operatorSessionCookie } : {},
371
+ ...stored.clientIdentityCookie ? { clientIdentityCookie: stored.clientIdentityCookie } : {}
372
+ };
373
+ });
374
+ }
375
+ async saveSession(owner, target, cookies) {
376
+ assertOwner(owner);
377
+ assertCookie(cookies.operatorSessionCookie, "lmt_operator_session");
378
+ assertCookie(cookies.clientIdentityCookie, "lmt_client_id");
379
+ if (!cookies.operatorSessionCookie && !cookies.clientIdentityCookie) return;
380
+ await this.update((record) => {
381
+ const sessions = sessionsOf(record, owner);
382
+ const index = sessions.findIndex((session) => sessionMatches(owner, session, target));
383
+ const next = storedSession(owner, target, cookies);
384
+ if (index >= 0) sessions[index] = next;
385
+ else sessions.push(next);
386
+ setSessions(record, owner, sessions);
387
+ });
388
+ }
389
+ async clearOperatorSession(owner, target) {
390
+ assertOwner(owner);
391
+ await this.update((record) => {
392
+ const sessions = sessionsOf(record, owner);
393
+ const index = sessions.findIndex((session) => sessionMatches(owner, session, target));
394
+ if (index < 0) return;
395
+ const { operatorSessionCookie: _cleared, ...remaining } = sessions[index];
396
+ sessions[index] = { ...remaining, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
397
+ setSessions(record, owner, sessions.filter(sessionHasCookies));
398
+ });
399
+ }
400
+ async removeTarget(owner, target) {
401
+ assertOwner(owner);
402
+ await this.update((record) => {
403
+ setSessions(record, owner, sessionsOf(record, owner).filter((session) => !sessionMatches(owner, session, target)));
404
+ });
405
+ }
406
+ async update(mutator) {
407
+ await this.withLock(async () => {
408
+ const record = await this.readRecord();
409
+ mutator(record);
410
+ await this.writeRecord(record);
411
+ });
412
+ }
413
+ async withLock(operation) {
414
+ await this.ensureDirectory();
415
+ const started = Date.now();
416
+ let lock;
417
+ while (!lock) {
418
+ try {
419
+ lock = await open(this.lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), OWNER_ONLY_FILE);
420
+ await lock.chmod(OWNER_ONLY_FILE);
421
+ await lock.writeFile(`${process.pid}
422
+ `);
423
+ await lock.sync();
424
+ } catch (error) {
425
+ if (error.code !== "EEXIST") {
426
+ throw new Error("Could not acquire the protected CLI profile storage lock.");
427
+ }
428
+ try {
429
+ await ensureOwnerOnly(this.lockPath, "file");
430
+ } catch (lockError) {
431
+ if (lockError.code === "ENOENT") continue;
432
+ throw lockError;
433
+ }
434
+ if (Date.now() - started >= this.lockWaitMs) {
435
+ throw new Error("Timed out waiting for the protected CLI profile storage lock.");
436
+ }
437
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
438
+ }
439
+ }
440
+ try {
441
+ return await operation();
442
+ } finally {
443
+ await lock.close().catch(() => void 0);
444
+ await unlink(this.lockPath).catch(() => void 0);
445
+ }
446
+ }
447
+ async ensureDirectory() {
448
+ let created = false;
449
+ try {
450
+ await mkdir(this.directory, { mode: OWNER_ONLY_DIRECTORY });
451
+ created = true;
452
+ } catch (error) {
453
+ if (error.code !== "EEXIST") {
454
+ throw new Error("Could not create the protected CLI profile directory.");
455
+ }
456
+ }
457
+ if (created) {
458
+ try {
459
+ await chmod(this.directory, OWNER_ONLY_DIRECTORY);
460
+ } catch {
461
+ throw new Error("Could not set protected CLI profile directory permissions.");
462
+ }
463
+ }
464
+ await ensureOwnerOnly(this.directory, "directory");
465
+ }
466
+ isRecoverableStoreFormatError(error) {
467
+ if (!(error instanceof Error)) return false;
468
+ 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.";
469
+ }
470
+ async archiveUnreadableStoreFile() {
471
+ const backupPath = `${this.filePath}.corrupt.${Date.now()}.bak`;
472
+ try {
473
+ await rename(this.filePath, backupPath);
474
+ } catch {
475
+ throw new Error("Could not repair the protected CLI profile file.");
476
+ }
477
+ }
478
+ async readRecordForSmsCredentialUpdate() {
479
+ try {
480
+ return await this.readRecord();
481
+ } catch (error) {
482
+ if (!this.isRecoverableStoreFormatError(error)) throw error;
483
+ await this.archiveUnreadableStoreFile();
484
+ return emptyRecord();
485
+ }
486
+ }
487
+ async readRecord() {
488
+ try {
489
+ const info = await lstat(this.filePath);
490
+ if (info.isSymbolicLink() || !info.isFile()) {
491
+ throw new Error("The protected CLI profile file must be a regular file.");
492
+ }
493
+ await ensureOwnerOnly(this.filePath, "file");
494
+ const handle = await open(this.filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
495
+ try {
496
+ return parseRecord(await handle.readFile("utf8"));
497
+ } finally {
498
+ await handle.close();
499
+ }
500
+ } catch (error) {
501
+ if (error.code === "ENOENT") return emptyRecord();
502
+ if (error instanceof Error && error.message.startsWith("The protected CLI profile")) throw error;
503
+ throw new Error("Could not read the protected CLI profile file.");
504
+ }
505
+ }
506
+ async writeRecord(record) {
507
+ const normalized = {
508
+ version: 2,
509
+ ...record.smsCredential ? { smsCredential: record.smsCredential } : {},
510
+ ...record.controllerSessions?.length ? {
511
+ controllerSessions: record.controllerSessions.map((session) => ({
512
+ environment: session.environment,
513
+ operatorId: session.operatorId,
514
+ ...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
515
+ ...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {},
516
+ updatedAt: session.updatedAt
517
+ }))
518
+ } : {},
519
+ profiles: Object.fromEntries(
520
+ Object.entries(record.profiles).map(([profileName, profileRecord]) => [
521
+ profileName,
522
+ {
523
+ sessions: profileRecord.sessions.map((session) => ({
524
+ environment: session.environment,
525
+ origin: session.origin,
526
+ operatorId: session.operatorId,
527
+ ...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
528
+ ...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {},
529
+ updatedAt: session.updatedAt
530
+ }))
531
+ }
532
+ ])
533
+ )
534
+ };
535
+ const temporaryPath = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
536
+ const content = `${JSON.stringify(normalized, null, 2)}
537
+ `;
538
+ let handle;
539
+ try {
540
+ handle = await open(temporaryPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), OWNER_ONLY_FILE);
541
+ await handle.chmod(OWNER_ONLY_FILE);
542
+ await handle.writeFile(content, "utf8");
543
+ await handle.sync();
544
+ await handle.close();
545
+ handle = void 0;
546
+ await rename(temporaryPath, this.filePath);
547
+ const directoryHandle = await open(this.directory, constants.O_RDONLY);
548
+ try {
549
+ await directoryHandle.sync();
550
+ } finally {
551
+ await directoryHandle.close();
552
+ }
553
+ } catch {
554
+ if (handle) await handle.close().catch(() => void 0);
555
+ await unlink(temporaryPath).catch(() => void 0);
556
+ throw new Error("Could not atomically update the protected CLI profile file.");
557
+ }
558
+ }
559
+ };
560
+
561
+ // ../../packages/contracts/src/price-cross-time.ts
562
+ var IST_OFFSET_MS = 330 * 6e4;
563
+
564
+ // ../../packages/contracts/src/index.ts
565
+ var OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS = 24 * 60 * 60;
566
+ var OPERATOR_SESSION_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
567
+ var DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS = 7 * 24 * 60 * 60;
568
+ var ZERODHA_SESSION_ADMISSION_HEADER = "x-lmt-session-admission";
569
+ var ZERODHA_SESSION_ADMISSION_VERIFY = "verify";
570
+ function isOperatorMarketProtection(value) {
571
+ return typeof value === "number" && Number.isInteger(value) && value >= 2 && value <= 90;
572
+ }
573
+ var ENTRY_CATEGORIES = [
574
+ "NSE EQ",
575
+ "BSE EQ",
576
+ "NFO FUT",
577
+ "NFO CE",
578
+ "NFO PE",
579
+ "BFO FUT",
580
+ "BFO CE",
581
+ "BFO PE"
582
+ ];
583
+ function validateEntryCategories(value) {
584
+ if (!Array.isArray(value) || value.some((category) => typeof category !== "string" || !ENTRY_CATEGORIES.includes(category)) || new Set(value).size !== value.length) {
585
+ throw new Error("entryCategories must be a list of supported Entry Categories with no duplicates.");
586
+ }
587
+ return [...value];
588
+ }
589
+ var API_PATHS = {
590
+ health: "/api/health",
591
+ unlockOperatorSession: "/api/operator-session/unlock",
592
+ reportOperatorActivity: "/api/operator-session/activity",
593
+ operatorSession: "/api/operator-session",
594
+ logoutUnauthenticatedOperator: "/api/unauthenticated-operator/logout",
595
+ zerodhaSession: "/api/zerodha/session",
596
+ logoutAuthenticatedOperator: "/api/authenticated-operator/logout",
597
+ operators: "/api/operators",
598
+ startOperatorLogin: "/api/operators/login",
599
+ operatorLoginEvents: "/api/operators/login/events",
600
+ operatorLoginOtp: "/api/operators/login/otp",
601
+ cancelOperatorLogin: "/api/operators/login/cancel",
602
+ zerodhaAccessToken: "/api/zerodha/access-token",
603
+ zerodhaAccessTokenRecovery: "/api/zerodha/access-token/recovery",
604
+ zerodhaAccessTokenPublication: "/api/zerodha/access-token/publication",
605
+ zerodhaCredentials: "/api/zerodha/credentials",
606
+ entryCategorySettings: "/api/settings/entry-categories",
607
+ operatorSettings: "/api/settings",
608
+ marketProtectionSettings: "/api/settings/market-protection",
609
+ operatorEgress: "/api/operator-egress",
610
+ validateInstrument: "/api/instruments/validate",
611
+ instrumentSearch: "/api/instruments/search",
612
+ startMarketFeed: "/api/market/feed/start",
613
+ stopMarketFeed: "/api/market/feed/stop",
614
+ marketFeedStream: "/api/market/feed/stream",
615
+ marketWatch: "/api/market-watch",
616
+ marketWatchRowCount: "/api/settings/market-watch-row-count",
617
+ previousInstruments: "/api/market-watch/previous-instruments",
618
+ candleHistoryLatest: "/api/candle-history/latest",
619
+ candleHistoryExport: "/api/candle-history/export",
620
+ candleExportOnDemand: "/api/candle-history/export/on-demand",
621
+ candleHistoryExportShare: "/api/candle-history/export/share",
622
+ candleHistory: "/api/candle-history",
623
+ zerodhaHistoricalCandles: "/api/zerodha/historical-candles",
624
+ applyMarketWatchRow: "/api/market-watch/apply",
625
+ addMarketWatchRow: "/api/market-watch/rows",
626
+ removeMarketWatchRow: "/api/market-watch/remove",
627
+ clearMarketWatchRow: "/api/market-watch/clear",
628
+ placeMarketWatchOrder: "/api/market-watch/orders/place",
629
+ retryMarketWatchPlace: "/api/market-watch/orders/place-retry",
630
+ exitMarketWatchOrder: "/api/market-watch/orders/exit",
631
+ cancelMarketWatchOrder: "/api/market-watch/orders/cancel",
632
+ assignPriceCrossPair: "/api/price-cross/pairs/assign",
633
+ unpairPriceCrossPair: "/api/price-cross/pairs/unpair",
634
+ priceCrossReport: "/api/price-cross/report",
635
+ priceCrossReportShare: "/api/price-cross/report/share",
636
+ priceCrossPairs: "/api/price-cross/pairs",
637
+ priceCrossHistory: "/api/price-cross/history",
638
+ priceCrossCloseChart: "/api/price-cross/close-chart"
639
+ };
640
+
641
+ // src/gate-api-client.ts
642
+ var SettingsActionError = class extends Error {
643
+ constructor(code, uncertain = false, messageOverride) {
644
+ super(messageOverride ?? (uncertain ? "Submission could not be confirmed. Read current Settings or token metadata before another submission." : code === "settings_conflict" ? "Settings changed elsewhere. Read saved Settings and submit again with its version." : code === "active_trading_confirmation" ? "Another device has active trading. Review it, then explicitly submit continueReplacement: true to continue." : code === "token_changed" ? "The token changed. Read token metadata and review a fresh submission." : code === "token_write_unresolved" ? "A token write is unresolved. Resolve interrupted save in Web before replacing." : code === "unresolved_order" ? "An Order remains unresolved. Reconcile it before replacing the token." : "The Settings or token operation was rejected."));
645
+ this.code = code;
646
+ this.uncertain = uncertain;
647
+ }
648
+ code;
649
+ uncertain;
650
+ };
651
+ var PRICE_CROSS_UNCERTAIN_MESSAGE = "Submission could not be confirmed and was not retried. Run pair list before another submission.";
652
+ var PriceCrossActionError = class extends Error {
653
+ constructor(code, uncertain = false, message) {
654
+ super(message ?? (uncertain ? PRICE_CROSS_UNCERTAIN_MESSAGE : "The Price Cross request was rejected."));
655
+ this.code = code;
656
+ this.uncertain = uncertain;
657
+ this.name = "PriceCrossActionError";
658
+ if (uncertain) this.recovery = "Read current and past pairs with pair list; the CLI never repeats a submission.";
659
+ }
660
+ code;
661
+ uncertain;
662
+ recovery;
663
+ };
664
+ var OperatorSessionRejectedError = class extends Error {
665
+ constructor(reason) {
666
+ super("The saved Operator Session was rejected by Gate.");
667
+ this.reason = reason;
668
+ this.name = "OperatorSessionRejectedError";
669
+ }
670
+ reason;
671
+ code = "operator_login_required";
672
+ };
673
+ var ZerodhaLoginFailedError = class extends Error {
674
+ constructor(message) {
675
+ super(message);
676
+ this.name = "ZerodhaLoginFailedError";
677
+ }
678
+ };
679
+ var ZerodhaLoginAttemptUnavailableError = class extends Error {
680
+ constructor(message) {
681
+ super(message);
682
+ this.name = "ZerodhaLoginAttemptUnavailableError";
683
+ }
684
+ };
685
+ var OperatorCredentialsRejectedError = class extends Error {
686
+ code = "incorrect_credentials";
687
+ constructor() {
688
+ super("Incorrect Operator Login Username or Password.");
689
+ this.name = "OperatorCredentialsRejectedError";
690
+ }
691
+ };
692
+ function redactSecrets(message, secrets) {
693
+ let redacted = message;
694
+ for (const secret of secrets) {
695
+ if (secret) redacted = redacted.split(secret).join("[REDACTED]");
696
+ }
697
+ return redacted.replace(
698
+ /((?:access_token|request_token|api_key|password|username|otp|totp|token|cookie)\s*[=:]\s*["']?)[^"'&\s,;}]*/gi,
699
+ "$1[REDACTED]"
700
+ );
701
+ }
702
+ function messageFromBody(body, fallback, secrets = []) {
703
+ const message = body && typeof body === "object" && "message" in body && typeof body.message === "string" ? body.message : fallback;
704
+ return redactSecrets(message, secrets);
705
+ }
706
+ function redactEvent(event, secrets) {
707
+ if ("message" in event && typeof event.message === "string") {
708
+ return { ...event, message: redactSecrets(event.message, secrets) };
709
+ }
710
+ return event;
711
+ }
712
+ async function readJson(response) {
713
+ try {
714
+ return await response.json();
715
+ } catch {
716
+ return void 0;
717
+ }
718
+ }
719
+ function transportErrorDetail(error) {
720
+ if (!(error instanceof Error)) return void 0;
721
+ const cause = error.cause;
722
+ if (cause instanceof Error) {
723
+ const code = cause.code;
724
+ return code ? String(code) : cause.message;
725
+ }
726
+ return error.message;
727
+ }
728
+ async function requestOrThrow(request, unavailableMessage, signal, targetUrl, secrets = []) {
729
+ try {
730
+ return await request();
731
+ } catch (error) {
732
+ if (signal?.aborted) throw new Error("The action exceeded its deadline.");
733
+ if (error instanceof OperatorSessionRejectedError || error instanceof ZerodhaLoginAttemptUnavailableError) throw error;
734
+ const detail = transportErrorDetail(error);
735
+ const at = targetUrl ? ` [${targetUrl}]` : "";
736
+ throw new Error(redactSecrets(detail ? `${unavailableMessage} (${detail})${at}` : `${unavailableMessage}${at}`, secrets));
737
+ }
738
+ }
739
+ function responseCookies(response) {
740
+ const responseHeaders = response.headers;
741
+ const setCookies = responseHeaders.getSetCookie?.() ?? [];
742
+ const values = setCookies.length > 0 ? setCookies : [response.headers.get("set-cookie") ?? ""];
743
+ const cookies = {};
744
+ for (const value of values) {
745
+ for (const match of value.matchAll(/(?:^|,\s*)(lmt_operator_session|lmt_client_id)=([^;,\s]+)/g)) {
746
+ if (match[1] === "lmt_operator_session") cookies.operatorSessionCookie = `${match[1]}=${match[2]}`;
747
+ if (match[1] === "lmt_client_id") cookies.clientIdentityCookie = `${match[1]}=${match[2]}`;
748
+ }
749
+ }
750
+ return cookies;
751
+ }
752
+ function clientIdFromCookie(cookie) {
753
+ return cookie?.startsWith("lmt_client_id=") ? cookie.slice("lmt_client_id=".length) : void 0;
754
+ }
755
+ function observedOperator(body) {
756
+ if (!body || typeof body !== "object" || !("operators" in body)) return void 0;
757
+ const list = body.operators;
758
+ if (!Array.isArray(list) || list.length !== 1) return void 0;
759
+ const first = list[0];
760
+ return first && typeof first === "object" && "id" in first && typeof first.id === "string" ? first.id : void 0;
761
+ }
762
+ function rejectionReason(body) {
763
+ return body && typeof body === "object" && "reason" in body && typeof body.reason === "string" ? body.reason : void 0;
764
+ }
765
+ function delay(ms, signal) {
766
+ return new Promise((resolve) => {
767
+ if (signal.aborted) {
768
+ resolve();
769
+ return;
770
+ }
771
+ const timer = setTimeout(resolve, ms);
772
+ signal.addEventListener("abort", () => {
773
+ clearTimeout(timer);
774
+ resolve();
775
+ }, { once: true });
776
+ });
777
+ }
778
+ function parseEventFrame(frame) {
779
+ const data = frame.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
780
+ if (!data) return void 0;
781
+ let event;
782
+ try {
783
+ event = JSON.parse(data);
784
+ } catch {
785
+ throw new Error("Origin returned an invalid Zerodha login update.");
786
+ }
787
+ if (!event || typeof event !== "object" || !("type" in event) || typeof event.attemptId !== "string" || !event.attemptId || "message" in event && typeof event.message !== "string" || event.type === "status" && (event.operatorId !== "OPERATOR_1" && event.operatorId !== "OPERATOR_2" || !["loading", "awaiting_otp", "succeeded", "failed"].includes(event.status) || event.failedOtpAttempts !== void 0 && (!Number.isInteger(event.failedOtpAttempts) || event.failedOtpAttempts < 0)) || event.type === "attempt_error" && (!["login_attempt_cancelled", "login_attempt_expired", "login_attempt_not_found"].includes(event.error) || typeof event.message !== "string") || event.type !== "status" && event.type !== "attempt_error") {
788
+ throw new Error("Origin returned an invalid Zerodha login update.");
789
+ }
790
+ if (event.type === "attempt_error") return { type: event.type, attemptId: event.attemptId, error: event.error, message: event.message };
791
+ return {
792
+ type: event.type,
793
+ attemptId: event.attemptId,
794
+ operatorId: event.operatorId,
795
+ status: event.status,
796
+ ...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts },
797
+ ...event.message === void 0 ? {} : { message: event.message }
798
+ };
799
+ }
800
+ var HTTP_CLIENT_KIND = "cli";
801
+ var BROWSER_CLIENT_KIND = "cli-browser";
802
+ function createGateApi(origin, fetchImpl = fetch, options = {}) {
803
+ let cookies = { ...options.cookies ?? {} };
804
+ let metadata = {};
805
+ const clientProfile = options.profile;
806
+ if (clientProfile !== void 0) assertClientProfileName(clientProfile);
807
+ const otpValues = /* @__PURE__ */ new Set();
808
+ let actionDeadlineSignal;
809
+ function combineSignals(...signals) {
810
+ const present = signals.filter((signal) => signal !== void 0);
811
+ if (present.length === 0) return void 0;
812
+ if (present.length === 1) return present[0];
813
+ return AbortSignal.any(present);
814
+ }
815
+ function sessionSecrets() {
816
+ return [cookies.operatorSessionCookie, cookies.clientIdentityCookie].filter((cookie) => Boolean(cookie)).flatMap((cookie) => [cookie, cookie.slice(cookie.indexOf("=") + 1)]);
817
+ }
818
+ function headers(extra) {
819
+ const result = new Headers(extra);
820
+ const cookie = [cookies.operatorSessionCookie, cookies.clientIdentityCookie].filter(Boolean).join("; ");
821
+ if (cookie) result.set("cookie", cookie);
822
+ result.set("x-lmt-client-kind", HTTP_CLIENT_KIND);
823
+ if (clientProfile) result.set("x-lmt-client-profile", clientProfile);
824
+ return result;
825
+ }
826
+ function rememberCookies(response, acceptSessionCookie = false) {
827
+ const received = responseCookies(response);
828
+ if (received.clientIdentityCookie) cookies.clientIdentityCookie = received.clientIdentityCookie;
829
+ if (acceptSessionCookie && received.operatorSessionCookie) cookies.operatorSessionCookie = received.operatorSessionCookie;
830
+ metadata = {
831
+ ...metadata.requestId ? { requestId: metadata.requestId } : {},
832
+ ...metadata.buildId ? { buildId: metadata.buildId } : {},
833
+ ...response.headers.get("x-request-id") ? { requestId: response.headers.get("x-request-id") } : {},
834
+ ...response.headers.get("x-lmt-build-id") ? { buildId: response.headers.get("x-lmt-build-id") } : {}
835
+ };
836
+ }
837
+ function actionSignal(explicitSignal) {
838
+ const timeout = options.timeoutMs && options.timeoutMs > 0 ? AbortSignal.timeout(options.timeoutMs) : void 0;
839
+ return combineSignals(options.signal, actionDeadlineSignal, explicitSignal, timeout);
840
+ }
841
+ async function rejectAbsentSession(response, path3) {
842
+ if (response.status !== 401 || path3 === API_PATHS.unlockOperatorSession || path3 === API_PATHS.logoutUnauthenticatedOperator) return;
843
+ const body = await readJson(response.clone());
844
+ if (body && typeof body === "object" && "error" in body && body.error === "operator_session_absent") {
845
+ delete cookies.operatorSessionCookie;
846
+ throw new OperatorSessionRejectedError(rejectionReason(body));
847
+ }
848
+ }
849
+ async function postJson(path3, body, explicitSignal, secrets = []) {
850
+ const signal = actionSignal(explicitSignal);
851
+ const url = `${origin}${path3}`;
852
+ const response = await requestOrThrow(
853
+ () => fetchImpl(url, {
854
+ method: "POST",
855
+ headers: headers({ "content-type": "application/json", accept: "application/json" }),
856
+ body: JSON.stringify(body),
857
+ ...signal ? { signal } : {}
858
+ }),
859
+ "Could not reach the selected Live Market Terminal Origin.",
860
+ signal,
861
+ url,
862
+ [...sessionSecrets(), ...otpValues, ...secrets]
863
+ );
864
+ rememberCookies(response);
865
+ await rejectAbsentSession(response, path3);
866
+ return response;
867
+ }
868
+ async function getJson(path3, explicitSignal, extraHeaders) {
869
+ const signal = actionSignal(explicitSignal);
870
+ const url = `${origin}${path3}`;
871
+ const response = await requestOrThrow(
872
+ () => fetchImpl(url, {
873
+ method: "GET",
874
+ headers: headers({ accept: "application/json", ...extraHeaders }),
875
+ cache: "no-store",
876
+ ...signal ? { signal } : {}
877
+ }),
878
+ "Could not reach the selected Live Market Terminal Origin.",
879
+ signal,
880
+ url,
881
+ [...sessionSecrets(), ...otpValues]
882
+ );
883
+ rememberCookies(response);
884
+ await rejectAbsentSession(response, path3);
885
+ return response;
886
+ }
887
+ async function settingsWrite(path3, body) {
888
+ let response;
889
+ try {
890
+ response = await fetchImpl(origin + path3, {
891
+ method: "PUT",
892
+ headers: headers({ "content-type": "application/json", accept: "application/json" }),
893
+ body: JSON.stringify(body),
894
+ signal: actionSignal()
895
+ });
896
+ } catch {
897
+ throw new SettingsActionError("submission_uncertain", true);
898
+ }
899
+ rememberCookies(response);
900
+ await rejectAbsentSession(response, path3);
901
+ if (!response.ok) {
902
+ const result = await readJson(response).catch(() => void 0);
903
+ const codes = [
904
+ "settings_conflict",
905
+ "settings_version_required",
906
+ "invalid_settings",
907
+ "trade_cycle_active",
908
+ "settings_save_failed",
909
+ "token_conflict",
910
+ "token_rejected",
911
+ "replacement_confirmation_required",
912
+ "token_write_pending",
913
+ "invalid_request",
914
+ "zerodha_session_absent",
915
+ "validation_failed",
916
+ "credentials_changed",
917
+ "replacement_blocked",
918
+ "token_changed",
919
+ "invalid_token_request",
920
+ "token_write_unresolved",
921
+ "active_trading_confirmation",
922
+ "validation_unavailable",
923
+ "unresolved_order",
924
+ "sharing_token_unavailable",
925
+ "token_recovery_unavailable",
926
+ "settings_recovery_required"
927
+ ];
928
+ const code = typeof result?.error === "string" && codes.includes(result.error) ? result.error : "operation_rejected";
929
+ const serverMessage = typeof result?.message === "string" && result.message.trim() ? result.message.trim() : void 0;
930
+ const relayMessage = serverMessage && (code === "token_rejected" || code === "validation_unavailable" || code === "invalid_token_request") ? serverMessage : void 0;
931
+ throw new SettingsActionError(code, code === "settings_save_failed" || response.status >= 500, relayMessage);
932
+ }
933
+ return response;
934
+ }
935
+ async function priceCrossMutation(path3, body, accept) {
936
+ let response;
937
+ try {
938
+ response = await fetchImpl(origin + path3, {
939
+ method: "POST",
940
+ headers: headers({ "content-type": "application/json", accept: "application/json" }),
941
+ body: JSON.stringify(body),
942
+ signal: actionSignal()
943
+ });
944
+ } catch {
945
+ throw new PriceCrossActionError("submission_uncertain", true);
946
+ }
947
+ rememberCookies(response);
948
+ await rejectAbsentSession(response, path3);
949
+ const parsed = await readJson(response);
950
+ if (response.ok) {
951
+ if (accept(parsed)) return parsed;
952
+ throw new PriceCrossActionError("submission_uncertain", true);
953
+ }
954
+ const refusal = priceCrossRefusal(parsed, response.status);
955
+ if (response.status >= 500 && refusal.code !== "store_unavailable") throw new PriceCrossActionError("submission_uncertain", true);
956
+ throw refusal;
957
+ }
958
+ function priceCrossRefusal(body, status) {
959
+ const result = body;
960
+ const code = typeof result?.error === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(result.error) ? result.error : `http_${status}`;
961
+ const message = typeof result?.message === "string" && result.message.trim() ? redactSecrets(result.message.trim(), sessionSecrets()) : void 0;
962
+ return new PriceCrossActionError(code, false, message);
963
+ }
964
+ async function priceCrossRead(path3, body) {
965
+ const response = body === void 0 ? await getJson(path3) : await postJson(path3, body);
966
+ if (!response.ok) throw priceCrossRefusal(await readJson(response), response.status);
967
+ return response;
968
+ }
969
+ function readRefusal(body, status, fallback) {
970
+ const result = body;
971
+ const code = typeof result?.error === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(result.error) ? result.error : fallback;
972
+ const reported = typeof result?.message === "string" && result.message.trim() ? redactSecrets(result.message.trim(), sessionSecrets()) : void 0;
973
+ if (status >= 500) return new SettingsActionError(code, false, `${reported ?? "Gate or Broker is temporarily unavailable."} Nothing was changed; try again.`);
974
+ return new SettingsActionError(code, false, reported);
975
+ }
976
+ function ordinarySettings(body) {
977
+ if (!body || !Number.isSafeInteger(body.settingsVersion) || !isOperatorMarketProtection(body.marketProtection) || !Array.isArray(body.entryCategories)) {
978
+ throw new SettingsActionError("invalid_response");
979
+ }
980
+ return { settingsVersion: body.settingsVersion, marketProtection: body.marketProtection, entryCategories: validateEntryCategories(body.entryCategories) };
981
+ }
982
+ const api = {
983
+ async getTokenMetadata() {
984
+ const response = await getJson(API_PATHS.zerodhaCredentials);
985
+ const body = await readJson(response);
986
+ if (!response.ok) throw readRefusal(body, response.status, "token_metadata_unavailable");
987
+ if (!body?.accessTokenMetadata) throw new SettingsActionError("token_metadata_unavailable");
988
+ const { fingerprint, version, updatedAt, writePending } = body.accessTokenMetadata;
989
+ return { fingerprint, version, updatedAt, ...writePending === void 0 ? {} : { writePending } };
990
+ },
991
+ async getSettings() {
992
+ const response = await getJson(API_PATHS.operatorSettings);
993
+ if (!response.ok) throw readRefusal(await readJson(response), response.status, "settings_read_failed");
994
+ return ordinarySettings(await readJson(response));
995
+ },
996
+ async saveSettings(request) {
997
+ const response = await settingsWrite(API_PATHS.operatorSettings, request);
998
+ try {
999
+ const body = await readJson(response);
1000
+ if (body.outcome !== "saved" && body.outcome !== "unchanged") throw new Error("Invalid outcome");
1001
+ return { outcome: body.outcome, settings: ordinarySettings(body.settings) };
1002
+ } catch {
1003
+ throw new SettingsActionError("submission_uncertain", true);
1004
+ }
1005
+ },
1006
+ async replaceToken(request) {
1007
+ const response = await settingsWrite(API_PATHS.zerodhaAccessToken, request);
1008
+ try {
1009
+ const body = await readJson(response);
1010
+ if (body.outcome !== "verified" && body.outcome !== "replaced") throw new Error("Invalid outcome");
1011
+ return { outcome: body.outcome, sharing: body.sharing, metadata: body.metadata, message: "Token operation completed." };
1012
+ } catch {
1013
+ throw new SettingsActionError("submission_uncertain", true);
1014
+ }
1015
+ },
1016
+ async exportCandles(request) {
1017
+ const response = await postJson(API_PATHS.candleExportOnDemand, request);
1018
+ const body = await readJson(response);
1019
+ if (!response.ok || !body || !("url" in body) || typeof body.url !== "string" || !("filename" in body)) {
1020
+ throw new Error(messageFromBody(body, "Could not publish Candle Export.", sessionSecrets()));
1021
+ }
1022
+ return body;
1023
+ },
1024
+ async listPriceCrossPairs() {
1025
+ const body = await readJson(await priceCrossRead(API_PATHS.priceCrossPairs));
1026
+ if (!body || !Array.isArray(body.active) || !Array.isArray(body.past)) throw new PriceCrossActionError("invalid_response");
1027
+ return { active: body.active, past: body.past };
1028
+ },
1029
+ assignPriceCrossPair(request) {
1030
+ return priceCrossMutation(API_PATHS.assignPriceCrossPair, request, (body) => !!body && typeof body === "object" && "pair" in body && !!body.pair && typeof body.pair.pairId === "string");
1031
+ },
1032
+ unpairPriceCrossPair(request) {
1033
+ return priceCrossMutation(API_PATHS.unpairPriceCrossPair, request, (body) => !!body && typeof body === "object" && "outcome" in body && (body.outcome === "unpaired" || body.outcome === "already_unpaired"));
1034
+ },
1035
+ async downloadPriceCrossReport(pairId) {
1036
+ const response = await priceCrossRead(API_PATHS.priceCrossReport, { pairId });
1037
+ const filename = /filename="([^"]+)"/.exec(response.headers.get("content-disposition") ?? "")?.[1];
1038
+ const count = Number(response.headers.get("x-price-cross-decision-count"));
1039
+ const bytes = new Uint8Array(await response.arrayBuffer());
1040
+ if (!filename || bytes.length === 0) throw new PriceCrossActionError("invalid_response");
1041
+ return { bytes, filename, decisionCount: response.headers.has("x-price-cross-decision-count") && Number.isSafeInteger(count) ? count : null };
1042
+ },
1043
+ publishPriceCrossReport(pairId) {
1044
+ return priceCrossMutation(API_PATHS.priceCrossReportShare, { pairId }, (body) => !!body && typeof body === "object" && "url" in body && typeof body.url === "string" && "filename" in body && typeof body.filename === "string");
1045
+ },
1046
+ setActionSignal(signal) {
1047
+ actionDeadlineSignal = signal;
1048
+ if (!signal) otpValues.clear();
1049
+ },
1050
+ setCookies(next) {
1051
+ cookies = { ...next };
1052
+ },
1053
+ getCookies() {
1054
+ return { ...cookies };
1055
+ },
1056
+ clearOperatorSession() {
1057
+ delete cookies.operatorSessionCookie;
1058
+ },
1059
+ responseMetadata() {
1060
+ return { ...metadata };
1061
+ },
1062
+ async unlock(username, password, signal) {
1063
+ const response = await postJson(API_PATHS.unlockOperatorSession, { username, password }, signal, [username, password]);
1064
+ const body = await readJson(response);
1065
+ if (!response.ok || !body || !("operatorSession" in body) || body.operatorSession !== "present") {
1066
+ if (body && typeof body === "object" && "error" in body && body.error === "incorrect_credentials") {
1067
+ throw new OperatorCredentialsRejectedError();
1068
+ }
1069
+ throw new Error(messageFromBody(body, "Could not unlock the Operator Session.", [username, password]));
1070
+ }
1071
+ const receivedCookie = responseCookies(response).operatorSessionCookie;
1072
+ if (!receivedCookie) throw new Error("Origin unlocked the Operator Session without returning its session cookie.");
1073
+ cookies.operatorSessionCookie = receivedCookie;
1074
+ otpValues.clear();
1075
+ },
1076
+ async reportOperatorActivity() {
1077
+ const response = await postJson(API_PATHS.reportOperatorActivity, {}, void 0);
1078
+ if (response.status === 404) return;
1079
+ const body = await readJson(response);
1080
+ if (response.status === 401 && body && typeof body === "object" && "error" in body && body.error === "operator_session_absent") {
1081
+ this.clearOperatorSession();
1082
+ throw new OperatorSessionRejectedError(rejectionReason(body));
1083
+ }
1084
+ if (!response.ok || !body || !("operatorSession" in body) || body.operatorSession !== "present") {
1085
+ throw new Error(messageFromBody(
1086
+ body,
1087
+ `Origin did not accept Operator Activity (HTTP ${response.status}).`,
1088
+ sessionSecrets()
1089
+ ));
1090
+ }
1091
+ rememberCookies(response, true);
1092
+ },
1093
+ async confirmOperatorSessionPresent(expectedOperatorId) {
1094
+ const healthPath = API_PATHS.health;
1095
+ const response = await getJson(healthPath);
1096
+ const body = await readJson(response);
1097
+ if (response.status === 401 && body && typeof body === "object" && "error" in body && body.error === "operator_session_absent") {
1098
+ this.clearOperatorSession();
1099
+ throw new OperatorSessionRejectedError(rejectionReason(body));
1100
+ }
1101
+ if (body && typeof body === "object" && "service" in body && body.service === "live-market-terminal-gate" && "operatorSession" in body && (body.operatorSession === "present" || body.operatorSession === "absent")) {
1102
+ if (!response.ok) {
1103
+ throw new Error(messageFromBody(body, `Gate health returned HTTP ${response.status} from GET ${origin}${healthPath}.`, sessionSecrets()));
1104
+ }
1105
+ if (body.operatorSession === "absent") {
1106
+ this.clearOperatorSession();
1107
+ throw new OperatorSessionRejectedError(rejectionReason(body));
1108
+ }
1109
+ const clientId = clientIdFromCookie(cookies.clientIdentityCookie);
1110
+ return { operatorId: expectedOperatorId, ...clientId ? { clientId } : {} };
1111
+ }
1112
+ const service = body && typeof body === "object" && "service" in body ? String(body.service) : void 0;
1113
+ const hint = service === "live-market-terminal-broker" ? " The public Origin /api proxy reached Broker instead of Gate; point its proxy at Gate (default 8788)." : !response.ok ? ` Gate health returned HTTP ${response.status}.` : "";
1114
+ throw new Error(messageFromBody(
1115
+ body,
1116
+ `Origin returned an invalid Gate health response from GET ${origin}${healthPath}.${hint}`,
1117
+ sessionSecrets()
1118
+ ));
1119
+ },
1120
+ async status(expectedOperatorId) {
1121
+ const session = await this.confirmOperatorSessionPresent(expectedOperatorId);
1122
+ const operatorsResponse = await getJson(API_PATHS.operators);
1123
+ const operatorsBody = await readJson(operatorsResponse);
1124
+ if (!operatorsResponse.ok) {
1125
+ throw new Error(messageFromBody(operatorsBody, "Could not inspect the authenticated Operator.", sessionSecrets()));
1126
+ }
1127
+ const operatorId = observedOperator(operatorsBody);
1128
+ if (!operatorId) throw new Error("Origin returned an invalid or ambiguous Operator Directory.");
1129
+ if (operatorId !== expectedOperatorId) {
1130
+ this.clearOperatorSession();
1131
+ throw new Error(`The saved session reached ${operatorId}, but this target expects ${expectedOperatorId}.`);
1132
+ }
1133
+ return session;
1134
+ },
1135
+ async logout() {
1136
+ if (!cookies.operatorSessionCookie) return;
1137
+ const response = await requestOrThrow(
1138
+ () => postJson(API_PATHS.logoutUnauthenticatedOperator, {}),
1139
+ "Could not reach the selected Live Market Terminal Origin to log out."
1140
+ );
1141
+ const body = await readJson(response);
1142
+ if (!response.ok || !body || typeof body !== "object" || !("operatorSession" in body) || body.operatorSession !== "absent") {
1143
+ throw new Error(messageFromBody(body, `Origin did not confirm Operator logout (HTTP ${response.status}).`, sessionSecrets()));
1144
+ }
1145
+ this.clearOperatorSession();
1146
+ },
1147
+ async startLogin() {
1148
+ const response = await requestOrThrow(
1149
+ () => postJson(API_PATHS.startOperatorLogin, {}),
1150
+ "Could not reach the selected Origin to start Login with Zerodha."
1151
+ );
1152
+ const body = await readJson(response);
1153
+ if (!response.ok || !body || typeof body !== "object" || !("status" in body) || body.status !== "started" || typeof body.attemptId !== "string") {
1154
+ const code = body && typeof body === "object" && "error" in body && typeof body.error === "string" && ["zerodha_credentials_missing", "secret_store_unavailable", "operator_not_configured"].includes(body.error) ? body.error : void 0;
1155
+ throw Object.assign(new Error(messageFromBody(body, "Could not start Login with Zerodha.", sessionSecrets())), code ? { code } : {});
1156
+ }
1157
+ otpValues.clear();
1158
+ return { attemptId: body.attemptId, operatorId: body.operatorId };
1159
+ },
1160
+ async submitOtp(attemptId, otp) {
1161
+ otpValues.add(otp);
1162
+ const response = await requestOrThrow(
1163
+ () => postJson(API_PATHS.operatorLoginOtp, { attemptId, otp }),
1164
+ "Could not reach the selected Origin to submit the OTP."
1165
+ );
1166
+ const body = await readJson(response);
1167
+ if (!response.ok || !body || typeof body !== "object" || !("status" in body) || body.status !== "loading") {
1168
+ const message = messageFromBody(body, "Could not submit the Zerodha OTP.", [...sessionSecrets(), ...otpValues]);
1169
+ if (body && typeof body === "object" && "error" in body && (body.error === "login_attempt_not_found" || body.error === "login_attempt_expired")) {
1170
+ throw new ZerodhaLoginAttemptUnavailableError(message);
1171
+ }
1172
+ throw new Error(message);
1173
+ }
1174
+ },
1175
+ async watchLoginAttempt(attemptId, onEvent, signal) {
1176
+ const requestSignal = actionSignal(signal) ?? signal;
1177
+ requestSignal.addEventListener("abort", () => otpValues.clear(), { once: true });
1178
+ const url = `${origin}${API_PATHS.operatorLoginEvents}?attemptId=${encodeURIComponent(attemptId)}`;
1179
+ while (!requestSignal.aborted) {
1180
+ let response;
1181
+ try {
1182
+ response = await fetchImpl(url, {
1183
+ method: "GET",
1184
+ headers: headers({ accept: "text/event-stream" }),
1185
+ cache: "no-store",
1186
+ signal: requestSignal
1187
+ });
1188
+ rememberCookies(response);
1189
+ } catch {
1190
+ if (requestSignal.aborted) return;
1191
+ await delay(500, requestSignal);
1192
+ continue;
1193
+ }
1194
+ await rejectAbsentSession(response, API_PATHS.operatorLoginEvents);
1195
+ if (!response.ok || !response.body) {
1196
+ const body = await readJson(response);
1197
+ otpValues.clear();
1198
+ const message = messageFromBody(body, "Could not receive Zerodha login updates.", sessionSecrets());
1199
+ if (body && typeof body === "object" && "error" in body && (body.error === "login_attempt_not_found" || body.error === "login_attempt_expired")) throw new ZerodhaLoginAttemptUnavailableError(message);
1200
+ throw new Error(message);
1201
+ }
1202
+ const reader = response.body.getReader();
1203
+ const cancelReader = () => {
1204
+ void reader.cancel().catch(() => void 0);
1205
+ };
1206
+ requestSignal.addEventListener("abort", cancelReader, { once: true });
1207
+ const decoder = new TextDecoder();
1208
+ let buffer = "";
1209
+ let completed = false;
1210
+ try {
1211
+ while (!requestSignal.aborted) {
1212
+ let chunk;
1213
+ try {
1214
+ chunk = await reader.read();
1215
+ } catch {
1216
+ break;
1217
+ }
1218
+ const { done, value } = chunk;
1219
+ buffer += decoder.decode(value, { stream: !done });
1220
+ let boundary = buffer.search(/\r?\n\r?\n/);
1221
+ while (boundary >= 0) {
1222
+ const frame = buffer.slice(0, boundary);
1223
+ const separator = buffer.slice(boundary).match(/^\r?\n\r?\n/)?.[0] ?? "\n\n";
1224
+ buffer = buffer.slice(boundary + separator.length);
1225
+ const event = parseEventFrame(frame);
1226
+ if (event) {
1227
+ const safeEvent = redactEvent(event, [...sessionSecrets(), ...otpValues]);
1228
+ onEvent(safeEvent);
1229
+ if (safeEvent.type === "attempt_error") {
1230
+ otpValues.clear();
1231
+ throw new ZerodhaLoginAttemptUnavailableError(safeEvent.message);
1232
+ }
1233
+ if (safeEvent.status === "failed") {
1234
+ otpValues.clear();
1235
+ throw new ZerodhaLoginFailedError(safeEvent.message ?? "Login with Zerodha failed.");
1236
+ }
1237
+ if (safeEvent.status === "succeeded") {
1238
+ otpValues.clear();
1239
+ completed = true;
1240
+ return;
1241
+ }
1242
+ }
1243
+ boundary = buffer.search(/\r?\n\r?\n/);
1244
+ }
1245
+ if (done) break;
1246
+ }
1247
+ } finally {
1248
+ requestSignal.removeEventListener("abort", cancelReader);
1249
+ await reader.cancel().catch(() => void 0);
1250
+ }
1251
+ if (completed || requestSignal.aborted) return;
1252
+ await delay(250, requestSignal);
1253
+ }
1254
+ },
1255
+ async confirmZerodhaSession() {
1256
+ const response = await requestOrThrow(
1257
+ () => getJson(API_PATHS.zerodhaSession, void 0, { [ZERODHA_SESSION_ADMISSION_HEADER]: ZERODHA_SESSION_ADMISSION_VERIFY }),
1258
+ "Could not verify the Zerodha Session with the selected Origin."
1259
+ );
1260
+ const body = await readJson(response);
1261
+ if (!response.ok || !body || typeof body !== "object" || !("zerodhaSession" in body) || body.zerodhaSession !== "present") {
1262
+ const code = body && typeof body === "object" && "error" in body && body.error === "zerodha_session_restoring" ? "verification" in body && body.verification === "unavailable" ? "zerodha_verification_unavailable" : "zerodha_session_restoring" : body && typeof body === "object" && "error" in body && body.error === "zerodha_session_absent" ? "zerodha_session_absent" : "zerodha_verification_unavailable";
1263
+ throw Object.assign(new Error(messageFromBody(body, "Zerodha Session is not present on the Broker.", sessionSecrets())), { code });
1264
+ }
1265
+ return body;
1266
+ }
1267
+ };
1268
+ return api;
1269
+ }
1270
+
1271
+ // src/controller-session.ts
1272
+ var CONTROLLER_LOGIN_STEP = "Run `lmt` in a terminal to log in";
1273
+ var CREDENTIAL_PROVISION_STEP = "Run `lmt credential provision` to provision the SMS credential";
1274
+ var LOGIN_REQUIRED_CODES = /* @__PURE__ */ new Set(["operator_login_required", "zerodha_session_absent"]);
1275
+ function isLoginRequired(code) {
1276
+ return LOGIN_REQUIRED_CODES.has(code);
1277
+ }
1278
+ function sessionUsable(report, need = "logged in") {
1279
+ return report.smsCredential === "present" && report.operatorSession === "valid" && (need === "operator session" || report.zerodha === "authenticated");
1280
+ }
1281
+ function loginStepFor(owner, environment) {
1282
+ const profile = profileNameOf(owner);
1283
+ return profile === void 0 ? CONTROLLER_LOGIN_STEP : `Run \`lmt login --env ${environment} --profile ${profile}\``;
1284
+ }
1285
+ var UNAUTHENTICATED_ZERODHA_CODES = /* @__PURE__ */ new Set(["zerodha_session_absent", "zerodha_session_restoring"]);
1286
+ async function reportSessionStatus(options) {
1287
+ const { target, owner, store } = options;
1288
+ const profile = profileNameOf(owner);
1289
+ const report = {
1290
+ version: 1,
1291
+ command: "status",
1292
+ outcome: "blocked",
1293
+ environment: target.environment,
1294
+ origin: target.origin,
1295
+ session: profile === void 0 ? "controller" : "profile",
1296
+ ...profile === void 0 ? {} : { profile },
1297
+ smsCredential: await store.getSmsCredential() ? "present" : "missing",
1298
+ operatorSession: "absent",
1299
+ zerodha: "unknown"
1300
+ };
1301
+ const cookies = await store.getSession(owner, target);
1302
+ if (cookies?.operatorSessionCookie) {
1303
+ const api = createGateApi(resolveCliApiOrigin(target), options.fetchImpl ?? fetch, {
1304
+ cookies,
1305
+ ...profile === void 0 ? {} : { profile },
1306
+ timeoutMs: options.timeoutMs,
1307
+ signal: options.signal
1308
+ });
1309
+ try {
1310
+ await api.status(target.operatorId);
1311
+ report.operatorSession = "valid";
1312
+ try {
1313
+ await api.confirmZerodhaSession();
1314
+ report.zerodha = "authenticated";
1315
+ } catch (error) {
1316
+ const code = error.code;
1317
+ if (typeof code !== "string" || !UNAUTHENTICATED_ZERODHA_CODES.has(code)) throw error;
1318
+ report.zerodha = "unauthenticated";
1319
+ }
1320
+ } catch (error) {
1321
+ if (error instanceof OperatorSessionRejectedError && !options.signal.aborted) {
1322
+ report.operatorSession = "expired";
1323
+ await store.clearOperatorSession(owner, target);
1324
+ } else {
1325
+ const message = options.signal.aborted ? "The action exceeded its deadline." : error instanceof Error ? error.message : "Could not determine the session status.";
1326
+ return {
1327
+ report: { ...report, outcome: "failure", error: { code: options.signal.aborted ? "deadline_exceeded" : "action_failed", message } },
1328
+ exitCode: 1
1329
+ };
1330
+ }
1331
+ }
1332
+ }
1333
+ if (report.smsCredential === "missing") return { report: { ...report, nextStep: CREDENTIAL_PROVISION_STEP }, exitCode: 4 };
1334
+ if (!sessionUsable(report)) {
1335
+ return { report: { ...report, nextStep: loginStepFor(owner, target.environment) }, exitCode: 4 };
1336
+ }
1337
+ return { report: { ...report, outcome: "success" }, exitCode: 0 };
1338
+ }
1339
+
1340
+ // src/sms.ts
1341
+ import { spawn } from "node:child_process";
1342
+ var SmsReadError = class extends Error {
1343
+ constructor(code, message) {
1344
+ super(message);
1345
+ this.code = code;
1346
+ this.name = "SmsReadError";
1347
+ }
1348
+ code;
1349
+ };
1350
+ async function readSmsSecret(options) {
1351
+ if (!options.credential.trim()) {
1352
+ throw new SmsReadError("sms_credential_missing", "No protected SMS credential has been provisioned.");
1353
+ }
1354
+ if (!options.name.trim() || !options.scope.trim() || !options.endpoint.trim()) {
1355
+ throw new SmsReadError("sms_unavailable", "SMS configuration is incomplete.");
1356
+ }
1357
+ if (options.signal?.aborted) throw new SmsReadError("sms_unavailable", "SMS access was cancelled.");
1358
+ const command = options.command ?? "npx";
1359
+ const args = options.command ? ["read", options.name] : ["--yes", "--package=@ai-digital-factory/sms-cli@0.1.2", "sms", "read", options.name];
1360
+ return new Promise((resolve, reject) => {
1361
+ let child;
1362
+ try {
1363
+ child = spawn(command, args, {
1364
+ env: {
1365
+ ...process.env,
1366
+ SMS_ENDPOINT: options.endpoint,
1367
+ SMS_CREDENTIAL: options.credential,
1368
+ SMS_SCOPE: options.scope
1369
+ },
1370
+ stdio: ["ignore", "pipe", "ignore"]
1371
+ });
1372
+ } catch {
1373
+ reject(new SmsReadError("sms_unavailable", "SMS access is unavailable."));
1374
+ return;
1375
+ }
1376
+ const chunks = [];
1377
+ let length = 0;
1378
+ let settled = false;
1379
+ const finish = (error, result) => {
1380
+ if (settled) return;
1381
+ settled = true;
1382
+ clearTimeout(timer);
1383
+ options.signal?.removeEventListener("abort", onAbort);
1384
+ if (error) reject(error);
1385
+ else resolve(result ?? "");
1386
+ };
1387
+ const onAbort = () => {
1388
+ child.kill("SIGTERM");
1389
+ finish(new SmsReadError("sms_unavailable", "SMS access exceeded the action deadline."));
1390
+ };
1391
+ const timer = setTimeout(() => {
1392
+ child.kill("SIGTERM");
1393
+ finish(new SmsReadError("sms_unavailable", "SMS access exceeded the action deadline."));
1394
+ }, options.timeoutMs ?? 3e4);
1395
+ options.signal?.addEventListener("abort", onAbort, { once: true });
1396
+ child.stdout.on("data", (chunk) => {
1397
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1398
+ length += buffer.length;
1399
+ if (length > 1048576) {
1400
+ child.kill("SIGTERM");
1401
+ finish(new SmsReadError("sms_unavailable", "SMS returned an oversized secret record."));
1402
+ return;
1403
+ }
1404
+ chunks.push(buffer);
1405
+ });
1406
+ child.on("error", () => finish(new SmsReadError("sms_unavailable", "SMS access is unavailable.")));
1407
+ child.on("close", (code) => {
1408
+ if (code !== 0) {
1409
+ finish(new SmsReadError("sms_unavailable", "SMS access is unavailable for the selected Gate scope."));
1410
+ return;
1411
+ }
1412
+ const value = Buffer.concat(chunks).toString("utf8").trimEnd();
1413
+ if (!value.trim()) {
1414
+ finish(new SmsReadError("sms_unavailable", "SMS returned an empty secret record."));
1415
+ return;
1416
+ }
1417
+ finish(void 0, value);
1418
+ });
1419
+ });
1420
+ }
1421
+ async function readOperatorLoginCredentials(input) {
1422
+ let raw;
1423
+ try {
1424
+ raw = await (input.secretReader ?? readSmsSecret)({
1425
+ name: "live-market-terminal-operator-directory",
1426
+ scope: input.gateScope,
1427
+ endpoint: input.smsEndpoint,
1428
+ credential: input.smsCredential,
1429
+ ...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
1430
+ ...input.signal ? { signal: input.signal } : {},
1431
+ ...input.command ? { command: input.command } : {}
1432
+ });
1433
+ } catch (error) {
1434
+ if (error instanceof SmsReadError) throw error;
1435
+ throw new SmsReadError("sms_unavailable", "Operator Directory is unavailable from the selected Gate scope.");
1436
+ }
1437
+ try {
1438
+ const parsed = JSON.parse(raw);
1439
+ if (!parsed || typeof parsed !== "object" || !("operators" in parsed) || !Array.isArray(parsed.operators)) {
1440
+ throw new Error();
1441
+ }
1442
+ const matches = parsed.operators.filter(
1443
+ (entry2) => entry2 && typeof entry2 === "object" && "operatorId" in entry2 && entry2.operatorId === input.operatorId
1444
+ );
1445
+ if (matches.length !== 1) throw new Error();
1446
+ const entry = matches[0];
1447
+ if (typeof entry.loginUsername !== "string" || !entry.loginUsername.trim() || typeof entry.loginPassword !== "string" || !entry.loginPassword.trim()) throw new Error();
1448
+ return { username: entry.loginUsername.trim(), password: entry.loginPassword };
1449
+ } catch {
1450
+ throw new SmsReadError("invalid_operator_directory", "The selected Gate Operator Directory is invalid or does not contain exactly one configured Operator.");
1451
+ }
1452
+ }
1453
+
1454
+ export {
1455
+ DEFAULT_CONFIG_PATH,
1456
+ LocalConfigError,
1457
+ resolveTargetOrigin,
1458
+ resolveCliTarget,
1459
+ resolveCliApiOrigin,
1460
+ isOperatorMarketProtection,
1461
+ ENTRY_CATEGORIES,
1462
+ CLIENT_PROFILE_NAME_ERROR,
1463
+ assertClientProfileName,
1464
+ SettingsActionError,
1465
+ PriceCrossActionError,
1466
+ OperatorSessionRejectedError,
1467
+ ZerodhaLoginFailedError,
1468
+ ZerodhaLoginAttemptUnavailableError,
1469
+ OperatorCredentialsRejectedError,
1470
+ BROWSER_CLIENT_KIND,
1471
+ createGateApi,
1472
+ CONTROLLER_SESSION,
1473
+ ownerOf,
1474
+ profileNameOf,
1475
+ CliProfileStore,
1476
+ CONTROLLER_LOGIN_STEP,
1477
+ CREDENTIAL_PROVISION_STEP,
1478
+ isLoginRequired,
1479
+ sessionUsable,
1480
+ reportSessionStatus,
1481
+ SmsReadError,
1482
+ readOperatorLoginCredentials
1483
+ };