@ai-digital-factory/lmt-cli 0.1.5 → 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.
- package/README.md +175 -51
- package/dist/chunk-4N3XUBM4.js +1483 -0
- package/dist/chunk-7J2U4G5L.js +1394 -0
- package/dist/chunk-AQIOT6YD.js +2799 -0
- package/dist/chunk-DUF5HVSQ.js +58 -0
- package/dist/controller-app-UTLE4YVF.js +1382 -0
- package/dist/credential-app-QEFV6LZM.js +6 -0
- package/dist/index.js +8 -3536
- package/dist/menu-actions-HV2AY4KJ.js +67 -0
- package/dist/src-MZYWVU46.js +380 -0
- package/dist/web-5HW6PMSA.js +9 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,3540 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __esm = (fn, res, err) => function __init() {
|
|
5
|
-
if (err) throw err[0];
|
|
6
|
-
try {
|
|
7
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
-
} catch (e) {
|
|
9
|
-
throw err = [e], e;
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
var __export = (target, all) => {
|
|
13
|
-
for (var name in all)
|
|
14
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
-
};
|
|
16
2
|
|
|
17
|
-
//
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// ../../packages/contracts/src/price-cross-time.ts
|
|
25
|
-
var IST_OFFSET_MS;
|
|
26
|
-
var init_price_cross_time = __esm({
|
|
27
|
-
"../../packages/contracts/src/price-cross-time.ts"() {
|
|
28
|
-
"use strict";
|
|
29
|
-
IST_OFFSET_MS = 330 * 6e4;
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// ../../packages/contracts/src/price-cross-live.ts
|
|
34
|
-
var init_price_cross_live = __esm({
|
|
35
|
-
"../../packages/contracts/src/price-cross-live.ts"() {
|
|
36
|
-
"use strict";
|
|
37
|
-
init_price_cross_time();
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// ../../packages/contracts/src/index.ts
|
|
42
|
-
function isOperatorMarketProtection(value) {
|
|
43
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 2 && value <= 90;
|
|
44
|
-
}
|
|
45
|
-
function validateEntryCategories(value) {
|
|
46
|
-
if (!Array.isArray(value) || value.some((category) => typeof category !== "string" || !ENTRY_CATEGORIES.includes(category)) || new Set(value).size !== value.length) {
|
|
47
|
-
throw new Error("entryCategories must be a list of supported Entry Categories with no duplicates.");
|
|
48
|
-
}
|
|
49
|
-
return [...value];
|
|
50
|
-
}
|
|
51
|
-
var OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS, OPERATOR_SESSION_EXPIRY_WARNING_MS, DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS, ZERODHA_SESSION_ADMISSION_HEADER, ZERODHA_SESSION_ADMISSION_VERIFY, ENTRY_CATEGORIES, API_PATHS;
|
|
52
|
-
var init_src = __esm({
|
|
53
|
-
"../../packages/contracts/src/index.ts"() {
|
|
54
|
-
"use strict";
|
|
55
|
-
init_price_cross();
|
|
56
|
-
init_price_cross_live();
|
|
57
|
-
init_price_cross_time();
|
|
58
|
-
OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS = 24 * 60 * 60;
|
|
59
|
-
OPERATOR_SESSION_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
|
|
60
|
-
DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS = 7 * 24 * 60 * 60;
|
|
61
|
-
ZERODHA_SESSION_ADMISSION_HEADER = "x-lmt-session-admission";
|
|
62
|
-
ZERODHA_SESSION_ADMISSION_VERIFY = "verify";
|
|
63
|
-
ENTRY_CATEGORIES = [
|
|
64
|
-
"NSE EQ",
|
|
65
|
-
"BSE EQ",
|
|
66
|
-
"NFO FUT",
|
|
67
|
-
"NFO CE",
|
|
68
|
-
"NFO PE",
|
|
69
|
-
"BFO FUT",
|
|
70
|
-
"BFO CE",
|
|
71
|
-
"BFO PE"
|
|
72
|
-
];
|
|
73
|
-
API_PATHS = {
|
|
74
|
-
health: "/api/health",
|
|
75
|
-
unlockOperatorSession: "/api/operator-session/unlock",
|
|
76
|
-
reportOperatorActivity: "/api/operator-session/activity",
|
|
77
|
-
operatorSession: "/api/operator-session",
|
|
78
|
-
logoutUnauthenticatedOperator: "/api/unauthenticated-operator/logout",
|
|
79
|
-
zerodhaSession: "/api/zerodha/session",
|
|
80
|
-
logoutAuthenticatedOperator: "/api/authenticated-operator/logout",
|
|
81
|
-
operators: "/api/operators",
|
|
82
|
-
startOperatorLogin: "/api/operators/login",
|
|
83
|
-
operatorLoginEvents: "/api/operators/login/events",
|
|
84
|
-
operatorLoginOtp: "/api/operators/login/otp",
|
|
85
|
-
cancelOperatorLogin: "/api/operators/login/cancel",
|
|
86
|
-
zerodhaAccessToken: "/api/zerodha/access-token",
|
|
87
|
-
zerodhaAccessTokenRecovery: "/api/zerodha/access-token/recovery",
|
|
88
|
-
zerodhaAccessTokenPublication: "/api/zerodha/access-token/publication",
|
|
89
|
-
zerodhaCredentials: "/api/zerodha/credentials",
|
|
90
|
-
entryCategorySettings: "/api/settings/entry-categories",
|
|
91
|
-
operatorSettings: "/api/settings",
|
|
92
|
-
marketProtectionSettings: "/api/settings/market-protection",
|
|
93
|
-
operatorEgress: "/api/operator-egress",
|
|
94
|
-
validateInstrument: "/api/instruments/validate",
|
|
95
|
-
startMarketFeed: "/api/market/feed/start",
|
|
96
|
-
stopMarketFeed: "/api/market/feed/stop",
|
|
97
|
-
marketFeedStream: "/api/market/feed/stream",
|
|
98
|
-
marketWatch: "/api/market-watch",
|
|
99
|
-
marketWatchRowCount: "/api/settings/market-watch-row-count",
|
|
100
|
-
previousInstruments: "/api/market-watch/previous-instruments",
|
|
101
|
-
candleHistoryLatest: "/api/candle-history/latest",
|
|
102
|
-
candleHistoryExport: "/api/candle-history/export",
|
|
103
|
-
candleExportOnDemand: "/api/candle-history/export/on-demand",
|
|
104
|
-
candleHistoryExportShare: "/api/candle-history/export/share",
|
|
105
|
-
candleHistory: "/api/candle-history",
|
|
106
|
-
zerodhaHistoricalCandles: "/api/zerodha/historical-candles",
|
|
107
|
-
applyMarketWatchRow: "/api/market-watch/apply",
|
|
108
|
-
clearMarketWatchRow: "/api/market-watch/clear",
|
|
109
|
-
placeMarketWatchOrder: "/api/market-watch/orders/place",
|
|
110
|
-
retryMarketWatchPlace: "/api/market-watch/orders/place-retry",
|
|
111
|
-
exitMarketWatchOrder: "/api/market-watch/orders/exit",
|
|
112
|
-
cancelMarketWatchOrder: "/api/market-watch/orders/cancel",
|
|
113
|
-
assignPriceCrossPair: "/api/price-cross/pairs/assign",
|
|
114
|
-
unpairPriceCrossPair: "/api/price-cross/pairs/unpair",
|
|
115
|
-
priceCrossReport: "/api/price-cross/report",
|
|
116
|
-
priceCrossReportShare: "/api/price-cross/report/share",
|
|
117
|
-
priceCrossPairs: "/api/price-cross/pairs",
|
|
118
|
-
priceCrossHistory: "/api/price-cross/history"
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// src/profile-name.ts
|
|
124
|
-
function assertClientProfileName(profile) {
|
|
125
|
-
if (!clientProfilePattern.test(profile)) throw new Error(CLIENT_PROFILE_NAME_ERROR);
|
|
126
|
-
}
|
|
127
|
-
var CLIENT_PROFILE_NAME_ERROR, clientProfilePattern;
|
|
128
|
-
var init_profile_name = __esm({
|
|
129
|
-
"src/profile-name.ts"() {
|
|
130
|
-
"use strict";
|
|
131
|
-
CLIENT_PROFILE_NAME_ERROR = "Profile must be 1\u201364 letters, digits, dots, underscores, or hyphens, starting with a letter or digit.";
|
|
132
|
-
clientProfilePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// src/gate-api-client.ts
|
|
137
|
-
function redactSecrets(message, secrets) {
|
|
138
|
-
let redacted = message;
|
|
139
|
-
for (const secret of secrets) {
|
|
140
|
-
if (secret) redacted = redacted.split(secret).join("[REDACTED]");
|
|
141
|
-
}
|
|
142
|
-
return redacted.replace(
|
|
143
|
-
/((?:access_token|request_token|api_key|password|username|otp|totp|token|cookie)\s*[=:]\s*["']?)[^"'&\s,;}]*/gi,
|
|
144
|
-
"$1[REDACTED]"
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
function messageFromBody(body, fallback, secrets = []) {
|
|
148
|
-
const message = body && typeof body === "object" && "message" in body && typeof body.message === "string" ? body.message : fallback;
|
|
149
|
-
return redactSecrets(message, secrets);
|
|
150
|
-
}
|
|
151
|
-
function redactEvent(event, secrets) {
|
|
152
|
-
if ("message" in event && typeof event.message === "string") {
|
|
153
|
-
return { ...event, message: redactSecrets(event.message, secrets) };
|
|
154
|
-
}
|
|
155
|
-
return event;
|
|
156
|
-
}
|
|
157
|
-
async function readJson(response) {
|
|
158
|
-
try {
|
|
159
|
-
return await response.json();
|
|
160
|
-
} catch {
|
|
161
|
-
return void 0;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function transportErrorDetail(error) {
|
|
165
|
-
if (!(error instanceof Error)) return void 0;
|
|
166
|
-
const cause = error.cause;
|
|
167
|
-
if (cause instanceof Error) {
|
|
168
|
-
const code = cause.code;
|
|
169
|
-
return code ? String(code) : cause.message;
|
|
170
|
-
}
|
|
171
|
-
return error.message;
|
|
172
|
-
}
|
|
173
|
-
async function requestOrThrow(request, unavailableMessage, signal, targetUrl, secrets = []) {
|
|
174
|
-
try {
|
|
175
|
-
return await request();
|
|
176
|
-
} catch (error) {
|
|
177
|
-
if (signal?.aborted) throw new Error("The action exceeded its deadline.");
|
|
178
|
-
if (error instanceof OperatorSessionRejectedError || error instanceof ZerodhaLoginAttemptUnavailableError) throw error;
|
|
179
|
-
const detail = transportErrorDetail(error);
|
|
180
|
-
const at = targetUrl ? ` [${targetUrl}]` : "";
|
|
181
|
-
throw new Error(redactSecrets(detail ? `${unavailableMessage} (${detail})${at}` : `${unavailableMessage}${at}`, secrets));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
function responseCookies(response) {
|
|
185
|
-
const responseHeaders = response.headers;
|
|
186
|
-
const setCookies = responseHeaders.getSetCookie?.() ?? [];
|
|
187
|
-
const values = setCookies.length > 0 ? setCookies : [response.headers.get("set-cookie") ?? ""];
|
|
188
|
-
const cookies = {};
|
|
189
|
-
for (const value of values) {
|
|
190
|
-
for (const match of value.matchAll(/(?:^|,\s*)(lmt_operator_session|lmt_client_id)=([^;,\s]+)/g)) {
|
|
191
|
-
if (match[1] === "lmt_operator_session") cookies.operatorSessionCookie = `${match[1]}=${match[2]}`;
|
|
192
|
-
if (match[1] === "lmt_client_id") cookies.clientIdentityCookie = `${match[1]}=${match[2]}`;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return cookies;
|
|
196
|
-
}
|
|
197
|
-
function clientIdFromCookie(cookie) {
|
|
198
|
-
return cookie?.startsWith("lmt_client_id=") ? cookie.slice("lmt_client_id=".length) : void 0;
|
|
199
|
-
}
|
|
200
|
-
function observedOperator(body) {
|
|
201
|
-
if (!body || typeof body !== "object" || !("operators" in body)) return void 0;
|
|
202
|
-
const list = body.operators;
|
|
203
|
-
if (!Array.isArray(list) || list.length !== 1) return void 0;
|
|
204
|
-
const first = list[0];
|
|
205
|
-
return first && typeof first === "object" && "id" in first && typeof first.id === "string" ? first.id : void 0;
|
|
206
|
-
}
|
|
207
|
-
function rejectionReason(body) {
|
|
208
|
-
return body && typeof body === "object" && "reason" in body && typeof body.reason === "string" ? body.reason : void 0;
|
|
209
|
-
}
|
|
210
|
-
function delay(ms, signal) {
|
|
211
|
-
return new Promise((resolve) => {
|
|
212
|
-
if (signal.aborted) {
|
|
213
|
-
resolve();
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const timer = setTimeout(resolve, ms);
|
|
217
|
-
signal.addEventListener("abort", () => {
|
|
218
|
-
clearTimeout(timer);
|
|
219
|
-
resolve();
|
|
220
|
-
}, { once: true });
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
function parseEventFrame(frame) {
|
|
224
|
-
const data = frame.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
225
|
-
if (!data) return void 0;
|
|
226
|
-
let event;
|
|
227
|
-
try {
|
|
228
|
-
event = JSON.parse(data);
|
|
229
|
-
} catch {
|
|
230
|
-
throw new Error("Origin returned an invalid Zerodha login update.");
|
|
231
|
-
}
|
|
232
|
-
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") {
|
|
233
|
-
throw new Error("Origin returned an invalid Zerodha login update.");
|
|
234
|
-
}
|
|
235
|
-
if (event.type === "attempt_error") return { type: event.type, attemptId: event.attemptId, error: event.error, message: event.message };
|
|
236
|
-
return {
|
|
237
|
-
type: event.type,
|
|
238
|
-
attemptId: event.attemptId,
|
|
239
|
-
operatorId: event.operatorId,
|
|
240
|
-
status: event.status,
|
|
241
|
-
...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts },
|
|
242
|
-
...event.message === void 0 ? {} : { message: event.message }
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
246
|
-
let cookies = { ...options.cookies ?? {} };
|
|
247
|
-
let metadata = {};
|
|
248
|
-
const clientProfile = options.profile;
|
|
249
|
-
if (clientProfile !== void 0) assertClientProfileName(clientProfile);
|
|
250
|
-
const otpValues = /* @__PURE__ */ new Set();
|
|
251
|
-
let actionDeadlineSignal;
|
|
252
|
-
function combineSignals2(...signals) {
|
|
253
|
-
const present = signals.filter((signal) => signal !== void 0);
|
|
254
|
-
if (present.length === 0) return void 0;
|
|
255
|
-
if (present.length === 1) return present[0];
|
|
256
|
-
return AbortSignal.any(present);
|
|
257
|
-
}
|
|
258
|
-
function sessionSecrets() {
|
|
259
|
-
return [cookies.operatorSessionCookie, cookies.clientIdentityCookie].filter((cookie) => Boolean(cookie)).flatMap((cookie) => [cookie, cookie.slice(cookie.indexOf("=") + 1)]);
|
|
260
|
-
}
|
|
261
|
-
function headers(extra) {
|
|
262
|
-
const result = new Headers(extra);
|
|
263
|
-
const cookie = [cookies.operatorSessionCookie, cookies.clientIdentityCookie].filter(Boolean).join("; ");
|
|
264
|
-
if (cookie) result.set("cookie", cookie);
|
|
265
|
-
result.set("x-lmt-client-kind", "cli");
|
|
266
|
-
if (clientProfile) result.set("x-lmt-client-profile", clientProfile);
|
|
267
|
-
return result;
|
|
268
|
-
}
|
|
269
|
-
function rememberCookies(response, acceptSessionCookie = false) {
|
|
270
|
-
const received = responseCookies(response);
|
|
271
|
-
if (received.clientIdentityCookie) cookies.clientIdentityCookie = received.clientIdentityCookie;
|
|
272
|
-
if (acceptSessionCookie && received.operatorSessionCookie) cookies.operatorSessionCookie = received.operatorSessionCookie;
|
|
273
|
-
metadata = {
|
|
274
|
-
...metadata.requestId ? { requestId: metadata.requestId } : {},
|
|
275
|
-
...metadata.buildId ? { buildId: metadata.buildId } : {},
|
|
276
|
-
...response.headers.get("x-request-id") ? { requestId: response.headers.get("x-request-id") } : {},
|
|
277
|
-
...response.headers.get("x-lmt-build-id") ? { buildId: response.headers.get("x-lmt-build-id") } : {}
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
function actionSignal(explicitSignal) {
|
|
281
|
-
const timeout = options.timeoutMs && options.timeoutMs > 0 ? AbortSignal.timeout(options.timeoutMs) : void 0;
|
|
282
|
-
return combineSignals2(options.signal, actionDeadlineSignal, explicitSignal, timeout);
|
|
283
|
-
}
|
|
284
|
-
async function rejectAbsentSession(response, path5) {
|
|
285
|
-
if (response.status !== 401 || path5 === API_PATHS.unlockOperatorSession || path5 === API_PATHS.logoutUnauthenticatedOperator) return;
|
|
286
|
-
const body = await readJson(response.clone());
|
|
287
|
-
if (body && typeof body === "object" && "error" in body && body.error === "operator_session_absent") {
|
|
288
|
-
delete cookies.operatorSessionCookie;
|
|
289
|
-
throw new OperatorSessionRejectedError(rejectionReason(body));
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
async function postJson(path5, body, explicitSignal, secrets = []) {
|
|
293
|
-
const signal = actionSignal(explicitSignal);
|
|
294
|
-
const url = `${origin}${path5}`;
|
|
295
|
-
const response = await requestOrThrow(
|
|
296
|
-
() => fetchImpl(url, {
|
|
297
|
-
method: "POST",
|
|
298
|
-
headers: headers({ "content-type": "application/json", accept: "application/json" }),
|
|
299
|
-
body: JSON.stringify(body),
|
|
300
|
-
...signal ? { signal } : {}
|
|
301
|
-
}),
|
|
302
|
-
"Could not reach the selected Live Market Terminal Origin.",
|
|
303
|
-
signal,
|
|
304
|
-
url,
|
|
305
|
-
[...sessionSecrets(), ...otpValues, ...secrets]
|
|
306
|
-
);
|
|
307
|
-
rememberCookies(response);
|
|
308
|
-
await rejectAbsentSession(response, path5);
|
|
309
|
-
return response;
|
|
310
|
-
}
|
|
311
|
-
async function getJson(path5, explicitSignal, extraHeaders) {
|
|
312
|
-
const signal = actionSignal(explicitSignal);
|
|
313
|
-
const url = `${origin}${path5}`;
|
|
314
|
-
const response = await requestOrThrow(
|
|
315
|
-
() => fetchImpl(url, {
|
|
316
|
-
method: "GET",
|
|
317
|
-
headers: headers({ accept: "application/json", ...extraHeaders }),
|
|
318
|
-
cache: "no-store",
|
|
319
|
-
...signal ? { signal } : {}
|
|
320
|
-
}),
|
|
321
|
-
"Could not reach the selected Live Market Terminal Origin.",
|
|
322
|
-
signal,
|
|
323
|
-
url,
|
|
324
|
-
[...sessionSecrets(), ...otpValues]
|
|
325
|
-
);
|
|
326
|
-
rememberCookies(response);
|
|
327
|
-
await rejectAbsentSession(response, path5);
|
|
328
|
-
return response;
|
|
329
|
-
}
|
|
330
|
-
async function settingsWrite(path5, body) {
|
|
331
|
-
let response;
|
|
332
|
-
try {
|
|
333
|
-
response = await fetchImpl(origin + path5, {
|
|
334
|
-
method: "PUT",
|
|
335
|
-
headers: headers({ "content-type": "application/json", accept: "application/json" }),
|
|
336
|
-
body: JSON.stringify(body),
|
|
337
|
-
signal: actionSignal()
|
|
338
|
-
});
|
|
339
|
-
} catch {
|
|
340
|
-
throw new SettingsActionError("submission_uncertain", true);
|
|
341
|
-
}
|
|
342
|
-
rememberCookies(response);
|
|
343
|
-
await rejectAbsentSession(response, path5);
|
|
344
|
-
if (!response.ok) {
|
|
345
|
-
const result = await readJson(response).catch(() => void 0);
|
|
346
|
-
const codes = [
|
|
347
|
-
"settings_conflict",
|
|
348
|
-
"settings_version_required",
|
|
349
|
-
"invalid_settings",
|
|
350
|
-
"trade_cycle_active",
|
|
351
|
-
"settings_save_failed",
|
|
352
|
-
"token_conflict",
|
|
353
|
-
"token_rejected",
|
|
354
|
-
"replacement_confirmation_required",
|
|
355
|
-
"token_write_pending",
|
|
356
|
-
"invalid_request",
|
|
357
|
-
"zerodha_session_absent",
|
|
358
|
-
"validation_failed",
|
|
359
|
-
"credentials_changed",
|
|
360
|
-
"replacement_blocked",
|
|
361
|
-
"token_changed",
|
|
362
|
-
"invalid_token_request",
|
|
363
|
-
"token_write_unresolved",
|
|
364
|
-
"active_trading_confirmation",
|
|
365
|
-
"validation_unavailable",
|
|
366
|
-
"unresolved_order",
|
|
367
|
-
"sharing_token_unavailable",
|
|
368
|
-
"token_recovery_unavailable",
|
|
369
|
-
"settings_recovery_required"
|
|
370
|
-
];
|
|
371
|
-
const code = typeof result?.error === "string" && codes.includes(result.error) ? result.error : "operation_rejected";
|
|
372
|
-
const serverMessage = typeof result?.message === "string" && result.message.trim() ? result.message.trim() : void 0;
|
|
373
|
-
const relayMessage = serverMessage && (code === "token_rejected" || code === "validation_unavailable" || code === "invalid_token_request") ? serverMessage : void 0;
|
|
374
|
-
throw new SettingsActionError(code, code === "settings_save_failed" || response.status >= 500, relayMessage);
|
|
375
|
-
}
|
|
376
|
-
return response;
|
|
377
|
-
}
|
|
378
|
-
async function priceCrossMutation(path5, body, accept) {
|
|
379
|
-
let response;
|
|
380
|
-
try {
|
|
381
|
-
response = await fetchImpl(origin + path5, {
|
|
382
|
-
method: "POST",
|
|
383
|
-
headers: headers({ "content-type": "application/json", accept: "application/json" }),
|
|
384
|
-
body: JSON.stringify(body),
|
|
385
|
-
signal: actionSignal()
|
|
386
|
-
});
|
|
387
|
-
} catch {
|
|
388
|
-
throw new PriceCrossActionError("submission_uncertain", true);
|
|
389
|
-
}
|
|
390
|
-
rememberCookies(response);
|
|
391
|
-
await rejectAbsentSession(response, path5);
|
|
392
|
-
const parsed = await readJson(response);
|
|
393
|
-
if (response.ok) {
|
|
394
|
-
if (accept(parsed)) return parsed;
|
|
395
|
-
throw new PriceCrossActionError("submission_uncertain", true);
|
|
396
|
-
}
|
|
397
|
-
const refusal = priceCrossRefusal(parsed, response.status);
|
|
398
|
-
if (response.status >= 500 && refusal.code !== "store_unavailable") throw new PriceCrossActionError("submission_uncertain", true);
|
|
399
|
-
throw refusal;
|
|
400
|
-
}
|
|
401
|
-
function priceCrossRefusal(body, status) {
|
|
402
|
-
const result = body;
|
|
403
|
-
const code = typeof result?.error === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(result.error) ? result.error : `http_${status}`;
|
|
404
|
-
const message = typeof result?.message === "string" && result.message.trim() ? redactSecrets(result.message.trim(), sessionSecrets()) : void 0;
|
|
405
|
-
return new PriceCrossActionError(code, false, message);
|
|
406
|
-
}
|
|
407
|
-
async function priceCrossRead(path5, body) {
|
|
408
|
-
const response = body === void 0 ? await getJson(path5) : await postJson(path5, body);
|
|
409
|
-
if (!response.ok) throw priceCrossRefusal(await readJson(response), response.status);
|
|
410
|
-
return response;
|
|
411
|
-
}
|
|
412
|
-
function ordinarySettings(body) {
|
|
413
|
-
if (!body || !Number.isSafeInteger(body.settingsVersion) || !Number.isInteger(body.rowCount) || body.rowCount < 1 || body.rowCount > 20 || !isOperatorMarketProtection(body.marketProtection) || !Array.isArray(body.entryCategories)) {
|
|
414
|
-
throw new SettingsActionError("invalid_response");
|
|
415
|
-
}
|
|
416
|
-
return { settingsVersion: body.settingsVersion, rowCount: body.rowCount, marketProtection: body.marketProtection, entryCategories: validateEntryCategories(body.entryCategories) };
|
|
417
|
-
}
|
|
418
|
-
const api = {
|
|
419
|
-
async getTokenMetadata() {
|
|
420
|
-
const response = await getJson(API_PATHS.zerodhaCredentials);
|
|
421
|
-
const body = await readJson(response);
|
|
422
|
-
if (!response.ok || !body?.accessTokenMetadata) throw new SettingsActionError("token_metadata_unavailable");
|
|
423
|
-
const { fingerprint, version, updatedAt, writePending } = body.accessTokenMetadata;
|
|
424
|
-
return { fingerprint, version, updatedAt, ...writePending === void 0 ? {} : { writePending } };
|
|
425
|
-
},
|
|
426
|
-
async getSettings() {
|
|
427
|
-
const response = await getJson(API_PATHS.operatorSettings);
|
|
428
|
-
if (!response.ok) throw new SettingsActionError("settings_read_failed");
|
|
429
|
-
return ordinarySettings(await readJson(response));
|
|
430
|
-
},
|
|
431
|
-
async saveSettings(request) {
|
|
432
|
-
const response = await settingsWrite(API_PATHS.operatorSettings, request);
|
|
433
|
-
try {
|
|
434
|
-
const body = await readJson(response);
|
|
435
|
-
if (body.outcome !== "saved" && body.outcome !== "unchanged") throw new Error("Invalid outcome");
|
|
436
|
-
return { outcome: body.outcome, settings: ordinarySettings(body.settings) };
|
|
437
|
-
} catch {
|
|
438
|
-
throw new SettingsActionError("submission_uncertain", true);
|
|
439
|
-
}
|
|
440
|
-
},
|
|
441
|
-
async replaceToken(request) {
|
|
442
|
-
const response = await settingsWrite(API_PATHS.zerodhaAccessToken, request);
|
|
443
|
-
try {
|
|
444
|
-
const body = await readJson(response);
|
|
445
|
-
if (body.outcome !== "verified" && body.outcome !== "replaced") throw new Error("Invalid outcome");
|
|
446
|
-
return { outcome: body.outcome, sharing: body.sharing, metadata: body.metadata, message: "Token operation completed." };
|
|
447
|
-
} catch {
|
|
448
|
-
throw new SettingsActionError("submission_uncertain", true);
|
|
449
|
-
}
|
|
450
|
-
},
|
|
451
|
-
async exportCandles(request) {
|
|
452
|
-
const response = await postJson(API_PATHS.candleExportOnDemand, request);
|
|
453
|
-
const body = await readJson(response);
|
|
454
|
-
if (!response.ok || !body || !("url" in body) || typeof body.url !== "string" || !("filename" in body)) {
|
|
455
|
-
throw new Error(messageFromBody(body, "Could not publish Candle Export.", sessionSecrets()));
|
|
456
|
-
}
|
|
457
|
-
return body;
|
|
458
|
-
},
|
|
459
|
-
async listPriceCrossPairs() {
|
|
460
|
-
const body = await readJson(await priceCrossRead(API_PATHS.priceCrossPairs));
|
|
461
|
-
if (!body || !Array.isArray(body.active) || !Array.isArray(body.past)) throw new PriceCrossActionError("invalid_response");
|
|
462
|
-
return { active: body.active, past: body.past };
|
|
463
|
-
},
|
|
464
|
-
assignPriceCrossPair(request) {
|
|
465
|
-
return priceCrossMutation(API_PATHS.assignPriceCrossPair, request, (body) => !!body && typeof body === "object" && "pair" in body && !!body.pair && typeof body.pair.pairId === "string");
|
|
466
|
-
},
|
|
467
|
-
unpairPriceCrossPair(request) {
|
|
468
|
-
return priceCrossMutation(API_PATHS.unpairPriceCrossPair, request, (body) => !!body && typeof body === "object" && "outcome" in body && (body.outcome === "unpaired" || body.outcome === "already_unpaired"));
|
|
469
|
-
},
|
|
470
|
-
async downloadPriceCrossReport(pairId) {
|
|
471
|
-
const response = await priceCrossRead(API_PATHS.priceCrossReport, { pairId });
|
|
472
|
-
const filename = /filename="([^"]+)"/.exec(response.headers.get("content-disposition") ?? "")?.[1];
|
|
473
|
-
const count = Number(response.headers.get("x-price-cross-decision-count"));
|
|
474
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
475
|
-
if (!filename || bytes.length === 0) throw new PriceCrossActionError("invalid_response");
|
|
476
|
-
return { bytes, filename, decisionCount: response.headers.has("x-price-cross-decision-count") && Number.isSafeInteger(count) ? count : null };
|
|
477
|
-
},
|
|
478
|
-
publishPriceCrossReport(pairId) {
|
|
479
|
-
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");
|
|
480
|
-
},
|
|
481
|
-
setActionSignal(signal) {
|
|
482
|
-
actionDeadlineSignal = signal;
|
|
483
|
-
if (!signal) otpValues.clear();
|
|
484
|
-
},
|
|
485
|
-
setCookies(next) {
|
|
486
|
-
cookies = { ...next };
|
|
487
|
-
},
|
|
488
|
-
getCookies() {
|
|
489
|
-
return { ...cookies };
|
|
490
|
-
},
|
|
491
|
-
clearOperatorSession() {
|
|
492
|
-
delete cookies.operatorSessionCookie;
|
|
493
|
-
},
|
|
494
|
-
responseMetadata() {
|
|
495
|
-
return { ...metadata };
|
|
496
|
-
},
|
|
497
|
-
async unlock(username, password, signal) {
|
|
498
|
-
const response = await postJson(API_PATHS.unlockOperatorSession, { username, password }, signal, [username, password]);
|
|
499
|
-
const body = await readJson(response);
|
|
500
|
-
if (!response.ok || !body || !("operatorSession" in body) || body.operatorSession !== "present") {
|
|
501
|
-
if (body && typeof body === "object" && "error" in body && body.error === "incorrect_credentials") {
|
|
502
|
-
throw new OperatorCredentialsRejectedError();
|
|
503
|
-
}
|
|
504
|
-
throw new Error(messageFromBody(body, "Could not unlock the Operator Session.", [username, password]));
|
|
505
|
-
}
|
|
506
|
-
const receivedCookie = responseCookies(response).operatorSessionCookie;
|
|
507
|
-
if (!receivedCookie) throw new Error("Origin unlocked the Operator Session without returning its session cookie.");
|
|
508
|
-
cookies.operatorSessionCookie = receivedCookie;
|
|
509
|
-
otpValues.clear();
|
|
510
|
-
},
|
|
511
|
-
async reportOperatorActivity() {
|
|
512
|
-
const response = await postJson(API_PATHS.reportOperatorActivity, {}, void 0);
|
|
513
|
-
if (response.status === 404) return;
|
|
514
|
-
const body = await readJson(response);
|
|
515
|
-
if (response.status === 401 && body && typeof body === "object" && "error" in body && body.error === "operator_session_absent") {
|
|
516
|
-
this.clearOperatorSession();
|
|
517
|
-
throw new OperatorSessionRejectedError(rejectionReason(body));
|
|
518
|
-
}
|
|
519
|
-
if (!response.ok || !body || !("operatorSession" in body) || body.operatorSession !== "present") {
|
|
520
|
-
throw new Error(messageFromBody(
|
|
521
|
-
body,
|
|
522
|
-
`Origin did not accept Operator Activity (HTTP ${response.status}).`,
|
|
523
|
-
sessionSecrets()
|
|
524
|
-
));
|
|
525
|
-
}
|
|
526
|
-
rememberCookies(response, true);
|
|
527
|
-
},
|
|
528
|
-
async confirmOperatorSessionPresent(expectedOperatorId) {
|
|
529
|
-
const healthPath = API_PATHS.health;
|
|
530
|
-
const response = await getJson(healthPath);
|
|
531
|
-
const body = await readJson(response);
|
|
532
|
-
if (response.status === 401 && body && typeof body === "object" && "error" in body && body.error === "operator_session_absent") {
|
|
533
|
-
this.clearOperatorSession();
|
|
534
|
-
throw new OperatorSessionRejectedError(rejectionReason(body));
|
|
535
|
-
}
|
|
536
|
-
if (body && typeof body === "object" && "service" in body && body.service === "live-market-terminal-gate" && "operatorSession" in body && (body.operatorSession === "present" || body.operatorSession === "absent")) {
|
|
537
|
-
if (!response.ok) {
|
|
538
|
-
throw new Error(messageFromBody(body, `Gate health returned HTTP ${response.status} from GET ${origin}${healthPath}.`, sessionSecrets()));
|
|
539
|
-
}
|
|
540
|
-
if (body.operatorSession === "absent") {
|
|
541
|
-
this.clearOperatorSession();
|
|
542
|
-
throw new OperatorSessionRejectedError(rejectionReason(body));
|
|
543
|
-
}
|
|
544
|
-
const clientId = clientIdFromCookie(cookies.clientIdentityCookie);
|
|
545
|
-
return { operatorId: expectedOperatorId, ...clientId ? { clientId } : {} };
|
|
546
|
-
}
|
|
547
|
-
const service = body && typeof body === "object" && "service" in body ? String(body.service) : void 0;
|
|
548
|
-
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}.` : "";
|
|
549
|
-
throw new Error(messageFromBody(
|
|
550
|
-
body,
|
|
551
|
-
`Origin returned an invalid Gate health response from GET ${origin}${healthPath}.${hint}`,
|
|
552
|
-
sessionSecrets()
|
|
553
|
-
));
|
|
554
|
-
},
|
|
555
|
-
async status(expectedOperatorId) {
|
|
556
|
-
const session = await this.confirmOperatorSessionPresent(expectedOperatorId);
|
|
557
|
-
const operatorsResponse = await getJson(API_PATHS.operators);
|
|
558
|
-
const operatorsBody = await readJson(operatorsResponse);
|
|
559
|
-
if (!operatorsResponse.ok) {
|
|
560
|
-
throw new Error(messageFromBody(operatorsBody, "Could not inspect the authenticated Operator.", sessionSecrets()));
|
|
561
|
-
}
|
|
562
|
-
const operatorId = observedOperator(operatorsBody);
|
|
563
|
-
if (!operatorId) throw new Error("Origin returned an invalid or ambiguous Operator Directory.");
|
|
564
|
-
if (operatorId !== expectedOperatorId) {
|
|
565
|
-
this.clearOperatorSession();
|
|
566
|
-
throw new Error(`The saved session reached ${operatorId}, but this target expects ${expectedOperatorId}.`);
|
|
567
|
-
}
|
|
568
|
-
return session;
|
|
569
|
-
},
|
|
570
|
-
async logout() {
|
|
571
|
-
if (!cookies.operatorSessionCookie) return;
|
|
572
|
-
const response = await requestOrThrow(
|
|
573
|
-
() => postJson(API_PATHS.logoutUnauthenticatedOperator, {}),
|
|
574
|
-
"Could not reach the selected Live Market Terminal Origin to log out."
|
|
575
|
-
);
|
|
576
|
-
const body = await readJson(response);
|
|
577
|
-
if (!response.ok || !body || typeof body !== "object" || !("operatorSession" in body) || body.operatorSession !== "absent") {
|
|
578
|
-
throw new Error(messageFromBody(body, `Origin did not confirm Operator logout (HTTP ${response.status}).`, sessionSecrets()));
|
|
579
|
-
}
|
|
580
|
-
this.clearOperatorSession();
|
|
581
|
-
},
|
|
582
|
-
async startLogin() {
|
|
583
|
-
const response = await requestOrThrow(
|
|
584
|
-
() => postJson(API_PATHS.startOperatorLogin, {}),
|
|
585
|
-
"Could not reach the selected Origin to start Login with Zerodha."
|
|
586
|
-
);
|
|
587
|
-
const body = await readJson(response);
|
|
588
|
-
if (!response.ok || !body || typeof body !== "object" || !("status" in body) || body.status !== "started" || typeof body.attemptId !== "string") {
|
|
589
|
-
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;
|
|
590
|
-
throw Object.assign(new Error(messageFromBody(body, "Could not start Login with Zerodha.", sessionSecrets())), code ? { code } : {});
|
|
591
|
-
}
|
|
592
|
-
otpValues.clear();
|
|
593
|
-
return { attemptId: body.attemptId, operatorId: body.operatorId };
|
|
594
|
-
},
|
|
595
|
-
async submitOtp(attemptId, otp) {
|
|
596
|
-
otpValues.add(otp);
|
|
597
|
-
const response = await requestOrThrow(
|
|
598
|
-
() => postJson(API_PATHS.operatorLoginOtp, { attemptId, otp }),
|
|
599
|
-
"Could not reach the selected Origin to submit the OTP."
|
|
600
|
-
);
|
|
601
|
-
const body = await readJson(response);
|
|
602
|
-
if (!response.ok || !body || typeof body !== "object" || !("status" in body) || body.status !== "loading") {
|
|
603
|
-
const message = messageFromBody(body, "Could not submit the Zerodha OTP.", [...sessionSecrets(), ...otpValues]);
|
|
604
|
-
if (body && typeof body === "object" && "error" in body && (body.error === "login_attempt_not_found" || body.error === "login_attempt_expired")) {
|
|
605
|
-
throw new ZerodhaLoginAttemptUnavailableError(message);
|
|
606
|
-
}
|
|
607
|
-
throw new Error(message);
|
|
608
|
-
}
|
|
609
|
-
},
|
|
610
|
-
async watchLoginAttempt(attemptId, onEvent, signal) {
|
|
611
|
-
const requestSignal = actionSignal(signal) ?? signal;
|
|
612
|
-
requestSignal.addEventListener("abort", () => otpValues.clear(), { once: true });
|
|
613
|
-
const url = `${origin}${API_PATHS.operatorLoginEvents}?attemptId=${encodeURIComponent(attemptId)}`;
|
|
614
|
-
while (!requestSignal.aborted) {
|
|
615
|
-
let response;
|
|
616
|
-
try {
|
|
617
|
-
response = await fetchImpl(url, {
|
|
618
|
-
method: "GET",
|
|
619
|
-
headers: headers({ accept: "text/event-stream" }),
|
|
620
|
-
cache: "no-store",
|
|
621
|
-
signal: requestSignal
|
|
622
|
-
});
|
|
623
|
-
rememberCookies(response);
|
|
624
|
-
} catch {
|
|
625
|
-
if (requestSignal.aborted) return;
|
|
626
|
-
await delay(500, requestSignal);
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
await rejectAbsentSession(response, API_PATHS.operatorLoginEvents);
|
|
630
|
-
if (!response.ok || !response.body) {
|
|
631
|
-
const body = await readJson(response);
|
|
632
|
-
otpValues.clear();
|
|
633
|
-
const message = messageFromBody(body, "Could not receive Zerodha login updates.", sessionSecrets());
|
|
634
|
-
if (body && typeof body === "object" && "error" in body && (body.error === "login_attempt_not_found" || body.error === "login_attempt_expired")) throw new ZerodhaLoginAttemptUnavailableError(message);
|
|
635
|
-
throw new Error(message);
|
|
636
|
-
}
|
|
637
|
-
const reader = response.body.getReader();
|
|
638
|
-
const cancelReader = () => {
|
|
639
|
-
void reader.cancel().catch(() => void 0);
|
|
640
|
-
};
|
|
641
|
-
requestSignal.addEventListener("abort", cancelReader, { once: true });
|
|
642
|
-
const decoder = new TextDecoder();
|
|
643
|
-
let buffer = "";
|
|
644
|
-
let completed = false;
|
|
645
|
-
try {
|
|
646
|
-
while (!requestSignal.aborted) {
|
|
647
|
-
let chunk;
|
|
648
|
-
try {
|
|
649
|
-
chunk = await reader.read();
|
|
650
|
-
} catch {
|
|
651
|
-
break;
|
|
652
|
-
}
|
|
653
|
-
const { done, value } = chunk;
|
|
654
|
-
buffer += decoder.decode(value, { stream: !done });
|
|
655
|
-
let boundary = buffer.search(/\r?\n\r?\n/);
|
|
656
|
-
while (boundary >= 0) {
|
|
657
|
-
const frame = buffer.slice(0, boundary);
|
|
658
|
-
const separator = buffer.slice(boundary).match(/^\r?\n\r?\n/)?.[0] ?? "\n\n";
|
|
659
|
-
buffer = buffer.slice(boundary + separator.length);
|
|
660
|
-
const event = parseEventFrame(frame);
|
|
661
|
-
if (event) {
|
|
662
|
-
const safeEvent = redactEvent(event, [...sessionSecrets(), ...otpValues]);
|
|
663
|
-
onEvent(safeEvent);
|
|
664
|
-
if (safeEvent.type === "attempt_error") {
|
|
665
|
-
otpValues.clear();
|
|
666
|
-
throw new ZerodhaLoginAttemptUnavailableError(safeEvent.message);
|
|
667
|
-
}
|
|
668
|
-
if (safeEvent.status === "failed") {
|
|
669
|
-
otpValues.clear();
|
|
670
|
-
throw new ZerodhaLoginFailedError(safeEvent.message ?? "Login with Zerodha failed.");
|
|
671
|
-
}
|
|
672
|
-
if (safeEvent.status === "succeeded") {
|
|
673
|
-
otpValues.clear();
|
|
674
|
-
completed = true;
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
boundary = buffer.search(/\r?\n\r?\n/);
|
|
679
|
-
}
|
|
680
|
-
if (done) break;
|
|
681
|
-
}
|
|
682
|
-
} finally {
|
|
683
|
-
requestSignal.removeEventListener("abort", cancelReader);
|
|
684
|
-
await reader.cancel().catch(() => void 0);
|
|
685
|
-
}
|
|
686
|
-
if (completed || requestSignal.aborted) return;
|
|
687
|
-
await delay(250, requestSignal);
|
|
688
|
-
}
|
|
689
|
-
},
|
|
690
|
-
async confirmZerodhaSession() {
|
|
691
|
-
const response = await requestOrThrow(
|
|
692
|
-
() => getJson(API_PATHS.zerodhaSession, void 0, { [ZERODHA_SESSION_ADMISSION_HEADER]: ZERODHA_SESSION_ADMISSION_VERIFY }),
|
|
693
|
-
"Could not verify the Zerodha Session with the selected Origin."
|
|
694
|
-
);
|
|
695
|
-
const body = await readJson(response);
|
|
696
|
-
if (!response.ok || !body || typeof body !== "object" || !("zerodhaSession" in body) || body.zerodhaSession !== "present") {
|
|
697
|
-
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";
|
|
698
|
-
throw Object.assign(new Error(messageFromBody(body, "Zerodha Session is not present on the Broker.", sessionSecrets())), { code });
|
|
699
|
-
}
|
|
700
|
-
return body;
|
|
701
|
-
}
|
|
702
|
-
};
|
|
703
|
-
return api;
|
|
704
|
-
}
|
|
705
|
-
var SettingsActionError, PRICE_CROSS_UNCERTAIN_MESSAGE, PriceCrossActionError, OperatorSessionRejectedError, ZerodhaLoginFailedError, ZerodhaLoginAttemptUnavailableError, OperatorCredentialsRejectedError;
|
|
706
|
-
var init_gate_api_client = __esm({
|
|
707
|
-
"src/gate-api-client.ts"() {
|
|
708
|
-
"use strict";
|
|
709
|
-
init_src();
|
|
710
|
-
init_profile_name();
|
|
711
|
-
SettingsActionError = class extends Error {
|
|
712
|
-
constructor(code, uncertain = false, messageOverride) {
|
|
713
|
-
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."));
|
|
714
|
-
this.code = code;
|
|
715
|
-
this.uncertain = uncertain;
|
|
716
|
-
}
|
|
717
|
-
code;
|
|
718
|
-
uncertain;
|
|
719
|
-
};
|
|
720
|
-
PRICE_CROSS_UNCERTAIN_MESSAGE = "Submission could not be confirmed and was not retried. Run pair list before another submission.";
|
|
721
|
-
PriceCrossActionError = class extends Error {
|
|
722
|
-
constructor(code, uncertain = false, message) {
|
|
723
|
-
super(message ?? (uncertain ? PRICE_CROSS_UNCERTAIN_MESSAGE : "The Price Cross request was rejected."));
|
|
724
|
-
this.code = code;
|
|
725
|
-
this.uncertain = uncertain;
|
|
726
|
-
this.name = "PriceCrossActionError";
|
|
727
|
-
if (uncertain) this.recovery = "Read current and past pairs with pair list; the CLI never repeats a submission.";
|
|
728
|
-
}
|
|
729
|
-
code;
|
|
730
|
-
uncertain;
|
|
731
|
-
recovery;
|
|
732
|
-
};
|
|
733
|
-
OperatorSessionRejectedError = class extends Error {
|
|
734
|
-
constructor(reason) {
|
|
735
|
-
super("The saved Operator Session was rejected by Gate.");
|
|
736
|
-
this.reason = reason;
|
|
737
|
-
this.name = "OperatorSessionRejectedError";
|
|
738
|
-
}
|
|
739
|
-
reason;
|
|
740
|
-
code = "operator_login_required";
|
|
741
|
-
};
|
|
742
|
-
ZerodhaLoginFailedError = class extends Error {
|
|
743
|
-
constructor(message) {
|
|
744
|
-
super(message);
|
|
745
|
-
this.name = "ZerodhaLoginFailedError";
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
ZerodhaLoginAttemptUnavailableError = class extends Error {
|
|
749
|
-
constructor(message) {
|
|
750
|
-
super(message);
|
|
751
|
-
this.name = "ZerodhaLoginAttemptUnavailableError";
|
|
752
|
-
}
|
|
753
|
-
};
|
|
754
|
-
OperatorCredentialsRejectedError = class extends Error {
|
|
755
|
-
code = "incorrect_credentials";
|
|
756
|
-
constructor() {
|
|
757
|
-
super("Incorrect Operator Login Username or Password.");
|
|
758
|
-
this.name = "OperatorCredentialsRejectedError";
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
// src/gate-api.ts
|
|
765
|
-
var init_gate_api = __esm({
|
|
766
|
-
"src/gate-api.ts"() {
|
|
767
|
-
"use strict";
|
|
768
|
-
init_gate_api_client();
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
// src/action-deadline.ts
|
|
773
|
-
function withinActionDeadline(operation, signal, deadlineMessage) {
|
|
774
|
-
return new Promise((resolve, reject) => {
|
|
775
|
-
const abort = () => reject(new Error(deadlineMessage));
|
|
776
|
-
if (signal.aborted) {
|
|
777
|
-
void operation.catch(() => void 0);
|
|
778
|
-
abort();
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
782
|
-
operation.then(
|
|
783
|
-
(value) => {
|
|
784
|
-
signal.removeEventListener("abort", abort);
|
|
785
|
-
resolve(value);
|
|
786
|
-
},
|
|
787
|
-
(error) => {
|
|
788
|
-
signal.removeEventListener("abort", abort);
|
|
789
|
-
reject(error);
|
|
790
|
-
}
|
|
791
|
-
);
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
var init_action_deadline = __esm({
|
|
795
|
-
"src/action-deadline.ts"() {
|
|
796
|
-
"use strict";
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
// src/zerodha-actions.ts
|
|
801
|
-
async function runZerodhaAction(api, action, operatorId, signal, attemptId, otp, options = {}) {
|
|
802
|
-
const started = Date.now();
|
|
803
|
-
let observedState;
|
|
804
|
-
let observedOperator2;
|
|
805
|
-
api.setActionSignal?.(signal);
|
|
806
|
-
try {
|
|
807
|
-
if (signal.aborted) throw new Error("Zerodha login observation exceeded its deadline.");
|
|
808
|
-
if (action === "status") {
|
|
809
|
-
const session = await withinActionDeadline(api.confirmZerodhaSession(), signal, "Zerodha login observation exceeded its deadline.");
|
|
810
|
-
if (session.operatorId && session.operatorId !== operatorId) throw new Error("Origin returned an unexpected Zerodha Session Operator.");
|
|
811
|
-
return { status: "succeeded", observedState: "present", ...session.operatorId ? { observedOperator: session.operatorId } : {}, elapsedMs: Date.now() - started };
|
|
812
|
-
}
|
|
813
|
-
if (action === "login") {
|
|
814
|
-
const attempt = await withinActionDeadline(api.startLogin(), signal, "Zerodha login observation exceeded its deadline.");
|
|
815
|
-
attemptId = attempt.attemptId;
|
|
816
|
-
if (attempt.operatorId !== operatorId) throw new Error("Zerodha login reached an unexpected Operator.");
|
|
817
|
-
}
|
|
818
|
-
if (!attemptId) throw new Error("Zerodha login requires an attempt identifier.");
|
|
819
|
-
if (action === "otp") {
|
|
820
|
-
if (!otp) throw new Error("Zerodha OTP input is required.");
|
|
821
|
-
await withinActionDeadline(api.submitOtp(attemptId, otp), signal, "Zerodha login observation exceeded its deadline.");
|
|
822
|
-
}
|
|
823
|
-
const observer = new AbortController();
|
|
824
|
-
const observationSignal = AbortSignal.any([signal, observer.signal]);
|
|
825
|
-
let result;
|
|
826
|
-
try {
|
|
827
|
-
await withinActionDeadline(api.watchLoginAttempt(attemptId, (event) => {
|
|
828
|
-
if (observer.signal.aborted || signal.aborted) return;
|
|
829
|
-
if (event.attemptId !== attemptId || event.type === "status" && event.operatorId !== operatorId) {
|
|
830
|
-
throw new Error("Origin returned an unexpected Zerodha login attempt.");
|
|
831
|
-
}
|
|
832
|
-
if (event.type === "status") {
|
|
833
|
-
observedState = event.status;
|
|
834
|
-
observedOperator2 = event.operatorId;
|
|
835
|
-
}
|
|
836
|
-
options.onObservation?.(event);
|
|
837
|
-
if (event.type === "status" && (event.status === "awaiting_otp" || event.status === "succeeded")) {
|
|
838
|
-
result = {
|
|
839
|
-
status: event.status,
|
|
840
|
-
attemptId,
|
|
841
|
-
observedOperator: observedOperator2,
|
|
842
|
-
observedState,
|
|
843
|
-
...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts },
|
|
844
|
-
...event.message ? { message: event.message } : {}
|
|
845
|
-
};
|
|
846
|
-
observer.abort();
|
|
847
|
-
}
|
|
848
|
-
}, observationSignal), observationSignal, "Zerodha login observation exceeded its deadline.");
|
|
849
|
-
} catch (error) {
|
|
850
|
-
if (!observer.signal.aborted || !result) throw error;
|
|
851
|
-
}
|
|
852
|
-
if (signal.aborted || !result) throw new Error("Zerodha login observation exceeded its deadline.");
|
|
853
|
-
if (result.status === "succeeded") {
|
|
854
|
-
const session = await withinActionDeadline(api.confirmZerodhaSession(), signal, "Zerodha login observation exceeded its deadline.");
|
|
855
|
-
if (session.operatorId && session.operatorId !== operatorId) throw new Error("Origin returned an unexpected Zerodha Session Operator.");
|
|
856
|
-
}
|
|
857
|
-
return { ...result, elapsedMs: Date.now() - started };
|
|
858
|
-
} catch (cause) {
|
|
859
|
-
const error = cause instanceof Error ? cause : new Error("Zerodha action failed.");
|
|
860
|
-
Object.assign(error, {
|
|
861
|
-
...attemptId ? { attemptId, recovery: "Observe this exact attempt with the same target and profile; start explicitly only if Gate reports it inactive." } : {},
|
|
862
|
-
...observedState ? { observedState } : {},
|
|
863
|
-
...observedOperator2 ? { observedOperator: observedOperator2 } : {},
|
|
864
|
-
elapsedMs: Date.now() - started
|
|
865
|
-
});
|
|
866
|
-
throw error;
|
|
867
|
-
} finally {
|
|
868
|
-
api.setActionSignal?.();
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
var init_zerodha_actions = __esm({
|
|
872
|
-
"src/zerodha-actions.ts"() {
|
|
873
|
-
"use strict";
|
|
874
|
-
init_action_deadline();
|
|
875
|
-
}
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
// src/credential-app.tsx
|
|
879
|
-
var credential_app_exports = {};
|
|
880
|
-
__export(credential_app_exports, {
|
|
881
|
-
CredentialProvisionApp: () => CredentialProvisionApp
|
|
882
|
-
});
|
|
883
|
-
import { useState } from "react";
|
|
884
|
-
import { Box, Text, useInput } from "ink";
|
|
885
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
886
|
-
function CredentialProvisionApp({ store }) {
|
|
887
|
-
const [credential, setCredential] = useState("");
|
|
888
|
-
const [state, setState] = useState("input");
|
|
889
|
-
const [error, setError] = useState("");
|
|
890
|
-
useInput((input, key) => {
|
|
891
|
-
if (state !== "input") return;
|
|
892
|
-
if (key.return) {
|
|
893
|
-
if (!credential.trim()) {
|
|
894
|
-
setError("Enter the SMS read credential.");
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
setState("saving");
|
|
898
|
-
setError("");
|
|
899
|
-
void store.saveSmsCredential(credential).then(() => {
|
|
900
|
-
setCredential("");
|
|
901
|
-
setState("saved");
|
|
902
|
-
}).catch((error2) => {
|
|
903
|
-
setCredential("");
|
|
904
|
-
setError(error2 instanceof Error ? error2.message : "Could not save the protected SMS credential.");
|
|
905
|
-
setState("input");
|
|
906
|
-
});
|
|
907
|
-
} else if (key.backspace || key.delete) {
|
|
908
|
-
setCredential((value) => value.slice(0, -1));
|
|
909
|
-
setError("");
|
|
910
|
-
} else if (input && !key.ctrl && !key.meta && !/[\r\n]/.test(input)) {
|
|
911
|
-
setCredential((value) => value + input);
|
|
912
|
-
setError("");
|
|
913
|
-
}
|
|
914
|
-
});
|
|
915
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
|
|
916
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Live Market Terminal" }),
|
|
917
|
-
state === "input" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
918
|
-
/* @__PURE__ */ jsx(Text, { children: "SMS read credential" }),
|
|
919
|
-
/* @__PURE__ */ jsxs(Text, { children: [
|
|
920
|
-
"\u2022".repeat(credential.length),
|
|
921
|
-
/* @__PURE__ */ jsx(Text, { inverse: true, children: " " })
|
|
922
|
-
] }),
|
|
923
|
-
/* @__PURE__ */ jsx(Text, { children: "Press Enter to save it in protected local profile storage." })
|
|
924
|
-
] }),
|
|
925
|
-
state === "saving" && /* @__PURE__ */ jsx(Text, { children: "Saving the protected SMS credential\u2026" }),
|
|
926
|
-
state === "saved" && /* @__PURE__ */ jsx(Text, { children: "SMS credential saved." }),
|
|
927
|
-
error ? /* @__PURE__ */ jsx(Text, { color: "red", children: error }) : null
|
|
928
|
-
] });
|
|
929
|
-
}
|
|
930
|
-
var init_credential_app = __esm({
|
|
931
|
-
"src/credential-app.tsx"() {
|
|
932
|
-
"use strict";
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
// src/app.tsx
|
|
937
|
-
var app_exports = {};
|
|
938
|
-
__export(app_exports, {
|
|
939
|
-
LoginApp: () => LoginApp
|
|
940
|
-
});
|
|
941
|
-
import { useEffect, useRef, useState as useState2 } from "react";
|
|
942
|
-
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
943
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
944
|
-
function formatZerodhaValidationReport(report) {
|
|
945
|
-
return [
|
|
946
|
-
"Scenario: zerodha-session",
|
|
947
|
-
`Outcome: ${report.outcome}`,
|
|
948
|
-
`Reason: ${report.reason}`,
|
|
949
|
-
`Target: ${report.target.environment} ${report.target.origin}`,
|
|
950
|
-
`Profile: ${report.profile}`,
|
|
951
|
-
`Observed Operator: ${report.observedOperator ?? "not observed"}`,
|
|
952
|
-
`Observed state: ${report.observedState ?? "not observed"}`,
|
|
953
|
-
`Attempt: ${report.attemptId ?? "not observed"}`,
|
|
954
|
-
`Elapsed: ${report.elapsedMs} ms`,
|
|
955
|
-
`Request ID: ${report.requestId ?? "unavailable"}`,
|
|
956
|
-
`Build ID: ${report.buildId ?? "unavailable"}`,
|
|
957
|
-
...report.failedOtpAttempts === void 0 ? [] : [`Failed OTP attempts: ${report.failedOtpAttempts}`],
|
|
958
|
-
...report.message ? [report.message] : [],
|
|
959
|
-
...report.nextStep ? [`Next step: ${report.nextStep}`] : []
|
|
960
|
-
].join("\n");
|
|
961
|
-
}
|
|
962
|
-
function formatValidationReport(report) {
|
|
963
|
-
const lines = [
|
|
964
|
-
`Outcome: ${report.outcome}`,
|
|
965
|
-
`Reason: ${report.reason}`,
|
|
966
|
-
`Target: ${report.target.environment} ${report.target.origin}`,
|
|
967
|
-
`Profile: ${report.profile}`,
|
|
968
|
-
`Expected Operator: ${report.expectedOperator}`,
|
|
969
|
-
`Observed Operator: ${report.observedOperator ?? "not observed"}`,
|
|
970
|
-
`Elapsed: ${report.elapsedMs} ms`,
|
|
971
|
-
`Request ID: ${report.requestId ?? "unavailable"}`,
|
|
972
|
-
`Build ID: ${report.buildId ?? "unavailable"}`,
|
|
973
|
-
`Temporary Profile: ${report.temporaryProfile}`,
|
|
974
|
-
"Observations:",
|
|
975
|
-
...report.observations.length ? report.observations.map((observation) => ` - ${observation}`) : [" - none"],
|
|
976
|
-
`Cleanup: ${report.cleanup.outcome}`,
|
|
977
|
-
`Removed Profiles: ${report.cleanup.removedProfiles.length ? report.cleanup.removedProfiles.join(", ") : "none"}`
|
|
978
|
-
];
|
|
979
|
-
if (report.cleanup.error) {
|
|
980
|
-
lines.push(`Cleanup Error: ${report.cleanup.error.code}: ${report.cleanup.error.message}`);
|
|
981
|
-
}
|
|
982
|
-
return lines.join("\n");
|
|
983
|
-
}
|
|
984
|
-
function LoginApp({ api, expectedOperatorId = "OPERATOR_1", actionTimeoutMs = 3e4, onResumeSession, onReportOperatorActivity, onStatus, onValidate, onValidateZerodha, onLogout }) {
|
|
985
|
-
const [step, setStep] = useState2(onResumeSession ? "checking" : "username");
|
|
986
|
-
const [username, setUsername] = useState2("");
|
|
987
|
-
const [password, setPassword] = useState2("");
|
|
988
|
-
const [otp, setOtp] = useState2("");
|
|
989
|
-
const [progress, setProgress] = useState2(null);
|
|
990
|
-
const [error, setError] = useState2("");
|
|
991
|
-
const [menuMessage, setMenuMessage] = useState2("");
|
|
992
|
-
const [validationReport, setValidationReport] = useState2(null);
|
|
993
|
-
const [zerodhaValidationReport, setZerodhaValidationReport] = useState2(null);
|
|
994
|
-
const [resumeAttemptInput, setResumeAttemptInput] = useState2("");
|
|
995
|
-
const validatingZerodha = useRef(false);
|
|
996
|
-
const [selectedMenuIndex, setSelectedMenuIndex] = useState2(0);
|
|
997
|
-
const attemptId = useRef(null);
|
|
998
|
-
const watcher = useRef(null);
|
|
999
|
-
const loginInputDeadline = useRef(null);
|
|
1000
|
-
const zerodhaLoginDeadline = useRef(null);
|
|
1001
|
-
useEffect(() => {
|
|
1002
|
-
return () => {
|
|
1003
|
-
watcher.current?.abort();
|
|
1004
|
-
if (loginInputDeadline.current) clearTimeout(loginInputDeadline.current.timer);
|
|
1005
|
-
if (zerodhaLoginDeadline.current) clearTimeout(zerodhaLoginDeadline.current.timer);
|
|
1006
|
-
api.setActionSignal?.();
|
|
1007
|
-
};
|
|
1008
|
-
}, []);
|
|
1009
|
-
useEffect(() => {
|
|
1010
|
-
if (!onResumeSession) return;
|
|
1011
|
-
void onResumeSession().then(({ step: step2, hint }) => {
|
|
1012
|
-
clearLoginDeadline();
|
|
1013
|
-
if (hint) setError(hint);
|
|
1014
|
-
else setError("");
|
|
1015
|
-
setStep(step2);
|
|
1016
|
-
}).catch(() => {
|
|
1017
|
-
clearLoginDeadline();
|
|
1018
|
-
setStep("username");
|
|
1019
|
-
});
|
|
1020
|
-
}, [onResumeSession]);
|
|
1021
|
-
useEffect(() => {
|
|
1022
|
-
if (step !== "menu" || !onReportOperatorActivity) return;
|
|
1023
|
-
void onReportOperatorActivity().catch(() => void 0);
|
|
1024
|
-
const interval = setInterval(() => {
|
|
1025
|
-
void onReportOperatorActivity().catch(() => void 0);
|
|
1026
|
-
}, 5 * 6e4);
|
|
1027
|
-
return () => clearInterval(interval);
|
|
1028
|
-
}, [step, onReportOperatorActivity]);
|
|
1029
|
-
function clearLoginDeadline() {
|
|
1030
|
-
if (loginInputDeadline.current) clearTimeout(loginInputDeadline.current.timer);
|
|
1031
|
-
loginInputDeadline.current = null;
|
|
1032
|
-
}
|
|
1033
|
-
function ensureLoginDeadline() {
|
|
1034
|
-
if (loginInputDeadline.current && !loginInputDeadline.current.signal.aborted) return loginInputDeadline.current.signal;
|
|
1035
|
-
const controller = new AbortController();
|
|
1036
|
-
const timer = setTimeout(() => controller.abort(), actionTimeoutMs);
|
|
1037
|
-
loginInputDeadline.current = { signal: controller.signal, timer };
|
|
1038
|
-
controller.signal.addEventListener("abort", () => {
|
|
1039
|
-
if (loginInputDeadline.current?.signal !== controller.signal) return;
|
|
1040
|
-
clearLoginDeadline();
|
|
1041
|
-
setUsername("");
|
|
1042
|
-
setPassword("");
|
|
1043
|
-
setError("Operator Login exceeded its deadline.");
|
|
1044
|
-
setStep("username");
|
|
1045
|
-
}, { once: true });
|
|
1046
|
-
return controller.signal;
|
|
1047
|
-
}
|
|
1048
|
-
function clearZerodhaLoginDeadline(controller) {
|
|
1049
|
-
const current = zerodhaLoginDeadline.current;
|
|
1050
|
-
if (!current || controller && current.controller !== controller) return;
|
|
1051
|
-
clearTimeout(current.timer);
|
|
1052
|
-
zerodhaLoginDeadline.current = null;
|
|
1053
|
-
api.setActionSignal?.();
|
|
1054
|
-
}
|
|
1055
|
-
async function unlock() {
|
|
1056
|
-
if (!password) {
|
|
1057
|
-
setError("Enter the Operator Login Password.");
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
setError("");
|
|
1061
|
-
setStep("unlocking");
|
|
1062
|
-
const signal = ensureLoginDeadline();
|
|
1063
|
-
try {
|
|
1064
|
-
await api.unlock(username.trim(), password, signal);
|
|
1065
|
-
if (signal.aborted) throw new Error("Operator Login exceeded its deadline.");
|
|
1066
|
-
clearLoginDeadline();
|
|
1067
|
-
setPassword("");
|
|
1068
|
-
setStep("menu");
|
|
1069
|
-
} catch (cause) {
|
|
1070
|
-
const timedOut = signal.aborted;
|
|
1071
|
-
clearLoginDeadline();
|
|
1072
|
-
setError(timedOut ? "Operator Login exceeded its deadline." : cause instanceof Error ? cause.message : "Could not unlock the Operator Session.");
|
|
1073
|
-
if (timedOut) {
|
|
1074
|
-
setUsername("");
|
|
1075
|
-
setPassword("");
|
|
1076
|
-
}
|
|
1077
|
-
setStep("username");
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
function applyLoginEvent(event, signal, action) {
|
|
1081
|
-
if (signal.aborted || event.type !== "status") return;
|
|
1082
|
-
attemptId.current = event.attemptId;
|
|
1083
|
-
setProgress({
|
|
1084
|
-
status: event.status === "succeeded" ? "succeeded" : "awaiting_otp",
|
|
1085
|
-
attemptId: event.attemptId,
|
|
1086
|
-
observedState: event.status,
|
|
1087
|
-
observedOperator: event.operatorId,
|
|
1088
|
-
...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts }
|
|
1089
|
-
});
|
|
1090
|
-
if (event.status === "loading") setStep(action === "login" ? "waiting" : "verifying");
|
|
1091
|
-
}
|
|
1092
|
-
async function runLoginAction(action, controller, code) {
|
|
1093
|
-
try {
|
|
1094
|
-
const result = await runZerodhaAction(
|
|
1095
|
-
api,
|
|
1096
|
-
action,
|
|
1097
|
-
expectedOperatorId,
|
|
1098
|
-
controller.signal,
|
|
1099
|
-
action === "otp" ? attemptId.current ?? void 0 : void 0,
|
|
1100
|
-
code,
|
|
1101
|
-
{ onObservation: (event) => applyLoginEvent(event, controller.signal, action) }
|
|
1102
|
-
);
|
|
1103
|
-
if (controller.signal.aborted) return;
|
|
1104
|
-
attemptId.current = result.attemptId ?? null;
|
|
1105
|
-
setProgress(result);
|
|
1106
|
-
setOtp("");
|
|
1107
|
-
if (result.status === "awaiting_otp") {
|
|
1108
|
-
setError(result.message ?? "");
|
|
1109
|
-
setStep("otp");
|
|
1110
|
-
} else {
|
|
1111
|
-
clearZerodhaLoginDeadline(controller);
|
|
1112
|
-
setStep("success");
|
|
1113
|
-
}
|
|
1114
|
-
} catch (cause) {
|
|
1115
|
-
if (controller.signal.aborted) return;
|
|
1116
|
-
clearZerodhaLoginDeadline(controller);
|
|
1117
|
-
setOtp("");
|
|
1118
|
-
const recovery = cause;
|
|
1119
|
-
if (recovery?.attemptId) attemptId.current = recovery.attemptId;
|
|
1120
|
-
setError(`${cause instanceof Error ? cause.message : "Login with Zerodha failed."}${recovery?.attemptId ? ` Attempt: ${recovery.attemptId}. ${recovery.recovery ?? "Observe this exact attempt with the same target and profile."}` : ""}`);
|
|
1121
|
-
setStep(cause instanceof OperatorSessionRejectedError ? "username" : "menu");
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
async function beginLogin() {
|
|
1125
|
-
validatingZerodha.current = false;
|
|
1126
|
-
setZerodhaValidationReport(null);
|
|
1127
|
-
setError("");
|
|
1128
|
-
setProgress(null);
|
|
1129
|
-
attemptId.current = null;
|
|
1130
|
-
setStep("starting");
|
|
1131
|
-
const controller = new AbortController();
|
|
1132
|
-
watcher.current?.abort();
|
|
1133
|
-
watcher.current = controller;
|
|
1134
|
-
const timer = setTimeout(() => {
|
|
1135
|
-
if (zerodhaLoginDeadline.current?.controller !== controller) return;
|
|
1136
|
-
controller.abort();
|
|
1137
|
-
clearZerodhaLoginDeadline(controller);
|
|
1138
|
-
setOtp("");
|
|
1139
|
-
setError(`Login with Zerodha exceeded its deadline.${attemptId.current ? ` Observe attempt ${attemptId.current} with the same target and profile.` : ""}`);
|
|
1140
|
-
setStep("menu");
|
|
1141
|
-
}, actionTimeoutMs);
|
|
1142
|
-
zerodhaLoginDeadline.current = { controller, timer };
|
|
1143
|
-
await runLoginAction("login", controller);
|
|
1144
|
-
}
|
|
1145
|
-
async function submitOtp() {
|
|
1146
|
-
if (!/^\d{6}$/.test(otp)) {
|
|
1147
|
-
setError("Enter the 6-digit Zerodha OTP.");
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
if (!attemptId.current || !watcher.current || watcher.current.signal.aborted) {
|
|
1151
|
-
setOtp("");
|
|
1152
|
-
setError("The Zerodha login attempt is no longer active.");
|
|
1153
|
-
setStep("menu");
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
const code = otp;
|
|
1157
|
-
setOtp("");
|
|
1158
|
-
setError("");
|
|
1159
|
-
setStep("verifying");
|
|
1160
|
-
if (validatingZerodha.current) await runValidation(watcher.current, attemptId.current, code);
|
|
1161
|
-
else await runLoginAction("otp", watcher.current, code);
|
|
1162
|
-
}
|
|
1163
|
-
function checkZerodhaSession() {
|
|
1164
|
-
setError("");
|
|
1165
|
-
setMenuMessage("");
|
|
1166
|
-
setStep("checking");
|
|
1167
|
-
const controller = new AbortController();
|
|
1168
|
-
watcher.current = controller;
|
|
1169
|
-
const timer = setTimeout(() => controller.abort(), actionTimeoutMs);
|
|
1170
|
-
void runZerodhaAction(api, "status", expectedOperatorId, controller.signal).then(() => {
|
|
1171
|
-
setMenuMessage("Zerodha Session confirmed by Gate.");
|
|
1172
|
-
setStep("menu");
|
|
1173
|
-
}).catch((cause) => {
|
|
1174
|
-
setError(cause instanceof Error ? cause.message : "Could not check the Zerodha Session.");
|
|
1175
|
-
setStep(cause instanceof OperatorSessionRejectedError ? "username" : "menu");
|
|
1176
|
-
}).finally(() => {
|
|
1177
|
-
clearTimeout(timer);
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
function checkOperatorSession() {
|
|
1181
|
-
if (!onStatus) return;
|
|
1182
|
-
setError("");
|
|
1183
|
-
setMenuMessage("");
|
|
1184
|
-
setValidationReport(null);
|
|
1185
|
-
setStep("checking");
|
|
1186
|
-
void onStatus().then((message) => {
|
|
1187
|
-
setMenuMessage(message);
|
|
1188
|
-
setStep("menu");
|
|
1189
|
-
}).catch((cause) => {
|
|
1190
|
-
setError(cause instanceof Error ? cause.message : "Could not check the Operator Session.");
|
|
1191
|
-
if (cause instanceof OperatorSessionRejectedError) {
|
|
1192
|
-
setUsername("");
|
|
1193
|
-
setPassword("");
|
|
1194
|
-
setStep("username");
|
|
1195
|
-
} else {
|
|
1196
|
-
setStep("menu");
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
function validateOperatorSession() {
|
|
1201
|
-
if (!onValidate) return;
|
|
1202
|
-
setError("");
|
|
1203
|
-
setMenuMessage("");
|
|
1204
|
-
setValidationReport(null);
|
|
1205
|
-
setStep("validating");
|
|
1206
|
-
void onValidate().then((result) => {
|
|
1207
|
-
setValidationReport(result);
|
|
1208
|
-
setStep("menu");
|
|
1209
|
-
}).catch((cause) => {
|
|
1210
|
-
setValidationReport(null);
|
|
1211
|
-
setError(cause instanceof Error ? cause.message : "Operator Session validation failed.");
|
|
1212
|
-
setStep("menu");
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
async function runValidation(controller, selectedAttempt, code) {
|
|
1216
|
-
if (!onValidateZerodha) return;
|
|
1217
|
-
try {
|
|
1218
|
-
const result = await onValidateZerodha({
|
|
1219
|
-
attemptId: selectedAttempt,
|
|
1220
|
-
otp: code,
|
|
1221
|
-
signal: controller.signal,
|
|
1222
|
-
onObservation: (event) => applyLoginEvent(event, controller.signal, code ? "otp" : "login")
|
|
1223
|
-
});
|
|
1224
|
-
if (watcher.current !== controller) return;
|
|
1225
|
-
setOtp("");
|
|
1226
|
-
setZerodhaValidationReport(result);
|
|
1227
|
-
attemptId.current = result.attemptId ?? null;
|
|
1228
|
-
if (!controller.signal.aborted && result.attemptId && ["awaiting_otp", "otp_rejected"].includes(result.reason)) {
|
|
1229
|
-
setStep("otp");
|
|
1230
|
-
} else {
|
|
1231
|
-
clearZerodhaLoginDeadline(controller);
|
|
1232
|
-
validatingZerodha.current = false;
|
|
1233
|
-
if (result.reason === "operator_login_required") {
|
|
1234
|
-
setUsername("");
|
|
1235
|
-
setPassword("");
|
|
1236
|
-
}
|
|
1237
|
-
setStep(result.reason === "operator_login_required" ? "username" : "menu");
|
|
1238
|
-
}
|
|
1239
|
-
} catch (cause) {
|
|
1240
|
-
if (watcher.current !== controller) return;
|
|
1241
|
-
clearZerodhaLoginDeadline(controller);
|
|
1242
|
-
validatingZerodha.current = false;
|
|
1243
|
-
setOtp("");
|
|
1244
|
-
const message = cause instanceof Error ? cause.message : "Zerodha Session validation failed.";
|
|
1245
|
-
setError(code ? message.replaceAll(code, "[REDACTED]") : message);
|
|
1246
|
-
setStep("menu");
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
async function beginValidation(selectedAttempt) {
|
|
1250
|
-
watcher.current?.abort();
|
|
1251
|
-
clearZerodhaLoginDeadline();
|
|
1252
|
-
const controller = new AbortController();
|
|
1253
|
-
const started = Date.now();
|
|
1254
|
-
watcher.current = controller;
|
|
1255
|
-
validatingZerodha.current = true;
|
|
1256
|
-
attemptId.current = selectedAttempt ?? null;
|
|
1257
|
-
setOtp("");
|
|
1258
|
-
setProgress(null);
|
|
1259
|
-
setError("");
|
|
1260
|
-
setMenuMessage("");
|
|
1261
|
-
setValidationReport(null);
|
|
1262
|
-
setZerodhaValidationReport(null);
|
|
1263
|
-
setStep("validatingZerodha");
|
|
1264
|
-
const timer = setTimeout(() => {
|
|
1265
|
-
if (zerodhaLoginDeadline.current?.controller !== controller) return;
|
|
1266
|
-
controller.abort();
|
|
1267
|
-
clearZerodhaLoginDeadline(controller);
|
|
1268
|
-
validatingZerodha.current = false;
|
|
1269
|
-
setOtp("");
|
|
1270
|
-
setZerodhaValidationReport((previous) => previous ? {
|
|
1271
|
-
...previous,
|
|
1272
|
-
outcome: "fail",
|
|
1273
|
-
reason: "deadline_exceeded",
|
|
1274
|
-
exitCode: 1,
|
|
1275
|
-
message: "The Zerodha Session validation exceeded its deadline.",
|
|
1276
|
-
elapsedMs: Date.now() - started,
|
|
1277
|
-
nextStep: `Observe attempt ${previous.attemptId} with the same target and profile; explicitly start again only if Gate reports it inactive.`
|
|
1278
|
-
} : previous);
|
|
1279
|
-
setError(`Zerodha Session validation exceeded its deadline.${attemptId.current ? ` Observe attempt ${attemptId.current} with the same target and profile.` : ""}`);
|
|
1280
|
-
setStep("menu");
|
|
1281
|
-
}, actionTimeoutMs);
|
|
1282
|
-
zerodhaLoginDeadline.current = { controller, timer };
|
|
1283
|
-
await runValidation(controller, selectedAttempt);
|
|
1284
|
-
}
|
|
1285
|
-
function resumeValidation() {
|
|
1286
|
-
setError("");
|
|
1287
|
-
setResumeAttemptInput("");
|
|
1288
|
-
setStep("validationAttempt");
|
|
1289
|
-
}
|
|
1290
|
-
function logoutOperatorSession() {
|
|
1291
|
-
if (!onLogout) return;
|
|
1292
|
-
setError("");
|
|
1293
|
-
setStep("loggingOut");
|
|
1294
|
-
void onLogout().then(() => {
|
|
1295
|
-
setUsername("");
|
|
1296
|
-
setPassword("");
|
|
1297
|
-
setMenuMessage("");
|
|
1298
|
-
setValidationReport(null);
|
|
1299
|
-
setZerodhaValidationReport(null);
|
|
1300
|
-
setStep("username");
|
|
1301
|
-
}).catch((cause) => {
|
|
1302
|
-
setError(cause instanceof Error ? cause.message : "Could not log out the Operator Session.");
|
|
1303
|
-
setStep("menu");
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
const menuActions = [
|
|
1307
|
-
{ shortcut: "1", label: "Login with Zerodha", run: () => {
|
|
1308
|
-
void beginLogin();
|
|
1309
|
-
} },
|
|
1310
|
-
...onStatus ? [{ shortcut: "2", label: "Check Operator Session", run: checkOperatorSession }] : [],
|
|
1311
|
-
...onValidate ? [{ shortcut: "3", label: "Validate Operator Session", run: validateOperatorSession }] : [],
|
|
1312
|
-
...onLogout ? [{ shortcut: "4", label: "Logout Operator Session", run: logoutOperatorSession }] : [],
|
|
1313
|
-
{ shortcut: "5", label: "Check Zerodha Session", run: checkZerodhaSession },
|
|
1314
|
-
...onValidateZerodha ? [
|
|
1315
|
-
{ shortcut: "6", label: "Validate Zerodha Session", run: () => {
|
|
1316
|
-
void beginValidation();
|
|
1317
|
-
} },
|
|
1318
|
-
{ shortcut: "7", label: "Resume Zerodha Validation", run: resumeValidation }
|
|
1319
|
-
] : []
|
|
1320
|
-
];
|
|
1321
|
-
const activeMenuIndex = Math.min(selectedMenuIndex, menuActions.length - 1);
|
|
1322
|
-
useInput2((input, key) => {
|
|
1323
|
-
if (step === "username") {
|
|
1324
|
-
if (key.ctrl && input === "u") {
|
|
1325
|
-
if (username.length > 0) ensureLoginDeadline();
|
|
1326
|
-
setUsername("");
|
|
1327
|
-
} else if (key.return) {
|
|
1328
|
-
if (!username.trim()) setError("Enter the Operator Login Username.");
|
|
1329
|
-
else {
|
|
1330
|
-
setError("");
|
|
1331
|
-
setStep("password");
|
|
1332
|
-
}
|
|
1333
|
-
} else if (key.backspace || key.delete) {
|
|
1334
|
-
if (username.length > 0) ensureLoginDeadline();
|
|
1335
|
-
setUsername((value) => value.slice(0, -1));
|
|
1336
|
-
} else if (input && !key.ctrl && !key.meta) {
|
|
1337
|
-
ensureLoginDeadline();
|
|
1338
|
-
setUsername((value) => value + input);
|
|
1339
|
-
}
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
if (step === "password") {
|
|
1343
|
-
if (key.escape) setStep("username");
|
|
1344
|
-
else if (key.ctrl && input === "u") {
|
|
1345
|
-
if (password.length > 0) ensureLoginDeadline();
|
|
1346
|
-
setPassword("");
|
|
1347
|
-
} else if (key.return) void unlock();
|
|
1348
|
-
else if (key.backspace || key.delete) {
|
|
1349
|
-
if (password.length > 0) ensureLoginDeadline();
|
|
1350
|
-
setPassword((value) => value.slice(0, -1));
|
|
1351
|
-
} else if (input && !key.ctrl && !key.meta) {
|
|
1352
|
-
ensureLoginDeadline();
|
|
1353
|
-
setPassword((value) => value + input);
|
|
1354
|
-
}
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
if (step === "validationAttempt") {
|
|
1358
|
-
if (key.escape) setStep("menu");
|
|
1359
|
-
else if (key.return) {
|
|
1360
|
-
if (!/^[a-zA-Z0-9_-]{1,200}$/.test(resumeAttemptInput)) setError("Enter an explicit valid Zerodha attempt ID.");
|
|
1361
|
-
else void beginValidation(resumeAttemptInput);
|
|
1362
|
-
} else if (key.backspace || key.delete) setResumeAttemptInput((value) => value.slice(0, -1));
|
|
1363
|
-
else if (input && !key.ctrl && !key.meta) setResumeAttemptInput((value) => (value + input).slice(0, 200));
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
if (step === "menu") {
|
|
1367
|
-
if (key.upArrow) {
|
|
1368
|
-
setSelectedMenuIndex((index) => (index - 1 + menuActions.length) % menuActions.length);
|
|
1369
|
-
} else if (key.downArrow) {
|
|
1370
|
-
setSelectedMenuIndex((index) => (index + 1) % menuActions.length);
|
|
1371
|
-
} else if (key.return) {
|
|
1372
|
-
menuActions[activeMenuIndex]?.run();
|
|
1373
|
-
} else {
|
|
1374
|
-
const shortcutAction = menuActions.find((action) => action.shortcut === input);
|
|
1375
|
-
shortcutAction?.run();
|
|
1376
|
-
}
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
if (step === "otp") {
|
|
1380
|
-
if (key.escape && validatingZerodha.current) {
|
|
1381
|
-
watcher.current?.abort();
|
|
1382
|
-
clearZerodhaLoginDeadline();
|
|
1383
|
-
validatingZerodha.current = false;
|
|
1384
|
-
setOtp("");
|
|
1385
|
-
setStep("menu");
|
|
1386
|
-
} else if (key.return) void submitOtp();
|
|
1387
|
-
else if (key.backspace || key.delete) setOtp((value) => value.slice(0, -1));
|
|
1388
|
-
else if (input) setOtp((value) => (value + input.replace(/\D/g, "")).slice(0, 6));
|
|
1389
|
-
}
|
|
1390
|
-
});
|
|
1391
|
-
if (step === "success") {
|
|
1392
|
-
return /* @__PURE__ */ jsx2(Text2, { children: "You have reached operator page" });
|
|
1393
|
-
}
|
|
1394
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
|
|
1395
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, children: "Live Market Terminal" }),
|
|
1396
|
-
step === "username" && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1397
|
-
/* @__PURE__ */ jsx2(Text2, { children: "Operator Login Username" }),
|
|
1398
|
-
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1399
|
-
username,
|
|
1400
|
-
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " })
|
|
1401
|
-
] }),
|
|
1402
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Enter to continue \xB7 Ctrl+U to clear" })
|
|
1403
|
-
] }),
|
|
1404
|
-
step === "password" && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1405
|
-
/* @__PURE__ */ jsx2(Text2, { children: "Operator Login Password" }),
|
|
1406
|
-
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1407
|
-
"\u2022".repeat(password.length),
|
|
1408
|
-
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " })
|
|
1409
|
-
] }),
|
|
1410
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Enter to log in \xB7 Esc to edit username \xB7 Ctrl+U to clear" })
|
|
1411
|
-
] }),
|
|
1412
|
-
step === "unlocking" && /* @__PURE__ */ jsx2(Text2, { children: "Unlocking Operator Session\u2026" }),
|
|
1413
|
-
step === "menu" && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1414
|
-
/* @__PURE__ */ jsx2(Text2, { children: "Select an action" }),
|
|
1415
|
-
menuActions.map((action, index) => {
|
|
1416
|
-
const selected = index === activeMenuIndex;
|
|
1417
|
-
return /* @__PURE__ */ jsxs2(Text2, { color: selected ? "cyan" : void 0, bold: selected, children: [
|
|
1418
|
-
selected ? "\u203A" : " ",
|
|
1419
|
-
" ",
|
|
1420
|
-
action.shortcut,
|
|
1421
|
-
" ",
|
|
1422
|
-
action.label
|
|
1423
|
-
] }, action.shortcut);
|
|
1424
|
-
}),
|
|
1425
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Use \u2191/\u2193 to move, Enter to select, or a number shortcut." }),
|
|
1426
|
-
menuMessage ? /* @__PURE__ */ jsx2(Text2, { children: menuMessage }) : null,
|
|
1427
|
-
validationReport ? /* @__PURE__ */ jsx2(Text2, { children: formatValidationReport(validationReport) }) : null
|
|
1428
|
-
] }),
|
|
1429
|
-
step === "checking" && /* @__PURE__ */ jsx2(Text2, { children: "Checking Operator Session\u2026" }),
|
|
1430
|
-
step === "validating" && /* @__PURE__ */ jsx2(Text2, { children: "Validating Operator Session\u2026" }),
|
|
1431
|
-
step === "validatingZerodha" && /* @__PURE__ */ jsx2(Text2, { children: "Validating Zerodha Session\u2026" }),
|
|
1432
|
-
step === "validationAttempt" && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1433
|
-
/* @__PURE__ */ jsx2(Text2, { children: "Zerodha attempt ID" }),
|
|
1434
|
-
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1435
|
-
resumeAttemptInput,
|
|
1436
|
-
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " })
|
|
1437
|
-
] }),
|
|
1438
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Enter to observe this attempt \xB7 Esc to return" })
|
|
1439
|
-
] }),
|
|
1440
|
-
zerodhaValidationReport ? /* @__PURE__ */ jsx2(Text2, { children: formatZerodhaValidationReport(zerodhaValidationReport) }) : null,
|
|
1441
|
-
step === "loggingOut" && /* @__PURE__ */ jsx2(Text2, { children: "Logging out of the Operator Session\u2026" }),
|
|
1442
|
-
step === "starting" && /* @__PURE__ */ jsx2(Text2, { children: "Starting Login with Zerodha\u2026" }),
|
|
1443
|
-
step === "waiting" && /* @__PURE__ */ jsx2(Text2, { children: "Connecting to Zerodha\u2026" }),
|
|
1444
|
-
step === "otp" && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1445
|
-
/* @__PURE__ */ jsx2(Text2, { children: "Zerodha OTP" }),
|
|
1446
|
-
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1447
|
-
"\u2022".repeat(otp.length),
|
|
1448
|
-
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " })
|
|
1449
|
-
] }),
|
|
1450
|
-
validatingZerodha.current ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Enter to submit \xB7 Esc to return and resume later" }) : null
|
|
1451
|
-
] }),
|
|
1452
|
-
step === "verifying" && /* @__PURE__ */ jsx2(Text2, { children: "Verifying Zerodha login\u2026" }),
|
|
1453
|
-
progress?.attemptId ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1454
|
-
"Attempt: ",
|
|
1455
|
-
progress.attemptId
|
|
1456
|
-
] }) : null,
|
|
1457
|
-
progress?.observedState ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1458
|
-
"Zerodha state: ",
|
|
1459
|
-
progress.observedState
|
|
1460
|
-
] }) : null,
|
|
1461
|
-
progress?.failedOtpAttempts !== void 0 ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1462
|
-
"Failed OTP attempts: ",
|
|
1463
|
-
progress.failedOtpAttempts
|
|
1464
|
-
] }) : null,
|
|
1465
|
-
error ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }) : null
|
|
1466
|
-
] });
|
|
1467
|
-
}
|
|
1468
|
-
var init_app = __esm({
|
|
1469
|
-
"src/app.tsx"() {
|
|
1470
|
-
"use strict";
|
|
1471
|
-
init_gate_api();
|
|
1472
|
-
init_zerodha_actions();
|
|
1473
|
-
}
|
|
1474
|
-
});
|
|
1475
|
-
|
|
1476
|
-
// src/index.ts
|
|
1477
|
-
import path4 from "node:path";
|
|
1478
|
-
import { realpathSync } from "node:fs";
|
|
1479
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1480
|
-
|
|
1481
|
-
// src/config.ts
|
|
1482
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1483
|
-
import path from "node:path";
|
|
1484
|
-
import { fileURLToPath } from "node:url";
|
|
1485
|
-
var repoRoot = path.resolve(
|
|
1486
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
1487
|
-
"../../../../"
|
|
1488
|
-
);
|
|
1489
|
-
var packagedConfigPath = fileURLToPath(new URL("../lmt.config.json", import.meta.url));
|
|
1490
|
-
var DEFAULT_CONFIG_PATH = existsSync(packagedConfigPath) ? packagedConfigPath : path.join(repoRoot, "lmt.config.json");
|
|
1491
|
-
function resolveCanonicalOrigin(value, environment) {
|
|
1492
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
1493
|
-
throw new Error(`lmt.config.json does not contain the ${environment} Origin.`);
|
|
1494
|
-
}
|
|
1495
|
-
let origin;
|
|
1496
|
-
try {
|
|
1497
|
-
origin = new URL(value.trim());
|
|
1498
|
-
} catch {
|
|
1499
|
-
throw new Error(`${environment} Origin must be an absolute HTTP URL.`);
|
|
1500
|
-
}
|
|
1501
|
-
if (!/^https?:$/.test(origin.protocol) || origin.username || origin.password || origin.hash) {
|
|
1502
|
-
throw new Error(
|
|
1503
|
-
`${environment} Origin must be an HTTP URL without credentials or a fragment.`
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
if (origin.pathname !== "/" || origin.search) {
|
|
1507
|
-
throw new Error(`${environment} Origin must not include a path or query string.`);
|
|
1508
|
-
}
|
|
1509
|
-
return origin.origin;
|
|
1510
|
-
}
|
|
1511
|
-
function resolveCliTarget(env = process.env, configPath = DEFAULT_CONFIG_PATH) {
|
|
1512
|
-
const environment = env.LMT_ENV?.trim();
|
|
1513
|
-
if (environment !== "local" && environment !== "dev") {
|
|
1514
|
-
if (environment === "prod") {
|
|
1515
|
-
throw new Error("The CLI supports local or dev targets only; prod is not permitted.");
|
|
1516
|
-
}
|
|
1517
|
-
throw new Error("Target must explicitly select --env local or --env dev; prod is not permitted.");
|
|
1518
|
-
}
|
|
1519
|
-
let config;
|
|
1520
|
-
try {
|
|
1521
|
-
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
1522
|
-
} catch {
|
|
1523
|
-
throw new Error("Could not read the deployment configuration lmt.config.json.");
|
|
1524
|
-
}
|
|
1525
|
-
const selected = config.environments?.[environment];
|
|
1526
|
-
if (!selected) {
|
|
1527
|
-
throw new Error(`lmt.config.json does not contain the ${environment} environment.`);
|
|
1528
|
-
}
|
|
1529
|
-
const origin = resolveCanonicalOrigin(selected.origin, environment);
|
|
1530
|
-
const smsEndpoint = typeof config.smsEndpoint === "string" ? config.smsEndpoint.trim() : "";
|
|
1531
|
-
if (!smsEndpoint) throw new Error("lmt.config.json does not contain smsEndpoint.");
|
|
1532
|
-
const gateScope = typeof selected.gateScope === "string" ? selected.gateScope.trim() : "";
|
|
1533
|
-
if (!gateScope) throw new Error(`lmt.config.json does not contain the ${environment} Gate scope.`);
|
|
1534
|
-
const operators = selected.brokerOperatorIds;
|
|
1535
|
-
if (!Array.isArray(operators) || operators.length !== 1 || operators[0] !== "OPERATOR_1") {
|
|
1536
|
-
throw new Error(`${environment} must have exactly one configured Operator: OPERATOR_1.`);
|
|
1537
|
-
}
|
|
1538
|
-
let smsUrl;
|
|
1539
|
-
try {
|
|
1540
|
-
smsUrl = new URL(smsEndpoint);
|
|
1541
|
-
} catch {
|
|
1542
|
-
throw new Error("smsEndpoint must be an absolute HTTP URL.");
|
|
1543
|
-
}
|
|
1544
|
-
if (!/^https?:$/.test(smsUrl.protocol) || smsUrl.username || smsUrl.password || smsUrl.hash) {
|
|
1545
|
-
throw new Error("smsEndpoint must be an HTTP URL without credentials or a fragment.");
|
|
1546
|
-
}
|
|
1547
|
-
return {
|
|
1548
|
-
environment,
|
|
1549
|
-
origin,
|
|
1550
|
-
gateScope,
|
|
1551
|
-
smsEndpoint: smsUrl.origin,
|
|
1552
|
-
operatorId: "OPERATOR_1"
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1555
|
-
function resolveCliApiOrigin(target) {
|
|
1556
|
-
return target.origin;
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
// src/operator-actions.ts
|
|
1560
|
-
init_gate_api();
|
|
1561
|
-
|
|
1562
|
-
// src/sms.ts
|
|
1563
|
-
import { spawn } from "node:child_process";
|
|
1564
|
-
var SmsReadError = class extends Error {
|
|
1565
|
-
constructor(code, message) {
|
|
1566
|
-
super(message);
|
|
1567
|
-
this.code = code;
|
|
1568
|
-
this.name = "SmsReadError";
|
|
1569
|
-
}
|
|
1570
|
-
code;
|
|
1571
|
-
};
|
|
1572
|
-
async function readSmsSecret(options) {
|
|
1573
|
-
if (!options.credential.trim()) {
|
|
1574
|
-
throw new SmsReadError("sms_credential_missing", "No protected SMS credential has been provisioned.");
|
|
1575
|
-
}
|
|
1576
|
-
if (!options.name.trim() || !options.scope.trim() || !options.endpoint.trim()) {
|
|
1577
|
-
throw new SmsReadError("sms_unavailable", "SMS configuration is incomplete.");
|
|
1578
|
-
}
|
|
1579
|
-
if (options.signal?.aborted) throw new SmsReadError("sms_unavailable", "SMS access was cancelled.");
|
|
1580
|
-
const command = options.command ?? "npx";
|
|
1581
|
-
const args = options.command ? ["read", options.name] : ["--yes", "--package=@ai-digital-factory/sms-cli@0.1.2", "sms", "read", options.name];
|
|
1582
|
-
return new Promise((resolve, reject) => {
|
|
1583
|
-
let child;
|
|
1584
|
-
try {
|
|
1585
|
-
child = spawn(command, args, {
|
|
1586
|
-
env: {
|
|
1587
|
-
...process.env,
|
|
1588
|
-
SMS_ENDPOINT: options.endpoint,
|
|
1589
|
-
SMS_CREDENTIAL: options.credential,
|
|
1590
|
-
SMS_SCOPE: options.scope
|
|
1591
|
-
},
|
|
1592
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1593
|
-
});
|
|
1594
|
-
} catch {
|
|
1595
|
-
reject(new SmsReadError("sms_unavailable", "SMS access is unavailable."));
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
const chunks = [];
|
|
1599
|
-
let length = 0;
|
|
1600
|
-
let settled = false;
|
|
1601
|
-
const finish = (error, result) => {
|
|
1602
|
-
if (settled) return;
|
|
1603
|
-
settled = true;
|
|
1604
|
-
clearTimeout(timer);
|
|
1605
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
1606
|
-
if (error) reject(error);
|
|
1607
|
-
else resolve(result ?? "");
|
|
1608
|
-
};
|
|
1609
|
-
const onAbort = () => {
|
|
1610
|
-
child.kill("SIGTERM");
|
|
1611
|
-
finish(new SmsReadError("sms_unavailable", "SMS access exceeded the action deadline."));
|
|
1612
|
-
};
|
|
1613
|
-
const timer = setTimeout(() => {
|
|
1614
|
-
child.kill("SIGTERM");
|
|
1615
|
-
finish(new SmsReadError("sms_unavailable", "SMS access exceeded the action deadline."));
|
|
1616
|
-
}, options.timeoutMs ?? 3e4);
|
|
1617
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1618
|
-
child.stdout.on("data", (chunk) => {
|
|
1619
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1620
|
-
length += buffer.length;
|
|
1621
|
-
if (length > 1048576) {
|
|
1622
|
-
child.kill("SIGTERM");
|
|
1623
|
-
finish(new SmsReadError("sms_unavailable", "SMS returned an oversized secret record."));
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
chunks.push(buffer);
|
|
1627
|
-
});
|
|
1628
|
-
child.on("error", () => finish(new SmsReadError("sms_unavailable", "SMS access is unavailable.")));
|
|
1629
|
-
child.on("close", (code) => {
|
|
1630
|
-
if (code !== 0) {
|
|
1631
|
-
finish(new SmsReadError("sms_unavailable", "SMS access is unavailable for the selected Gate scope."));
|
|
1632
|
-
return;
|
|
1633
|
-
}
|
|
1634
|
-
const value = Buffer.concat(chunks).toString("utf8").trimEnd();
|
|
1635
|
-
if (!value.trim()) {
|
|
1636
|
-
finish(new SmsReadError("sms_unavailable", "SMS returned an empty secret record."));
|
|
1637
|
-
return;
|
|
1638
|
-
}
|
|
1639
|
-
finish(void 0, value);
|
|
1640
|
-
});
|
|
1641
|
-
});
|
|
1642
|
-
}
|
|
1643
|
-
async function readOperatorLoginCredentials(input) {
|
|
1644
|
-
let raw;
|
|
1645
|
-
try {
|
|
1646
|
-
raw = await (input.secretReader ?? readSmsSecret)({
|
|
1647
|
-
name: "live-market-terminal-operator-directory",
|
|
1648
|
-
scope: input.gateScope,
|
|
1649
|
-
endpoint: input.smsEndpoint,
|
|
1650
|
-
credential: input.smsCredential,
|
|
1651
|
-
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
1652
|
-
...input.signal ? { signal: input.signal } : {},
|
|
1653
|
-
...input.command ? { command: input.command } : {}
|
|
1654
|
-
});
|
|
1655
|
-
} catch (error) {
|
|
1656
|
-
if (error instanceof SmsReadError) throw error;
|
|
1657
|
-
throw new SmsReadError("sms_unavailable", "Operator Directory is unavailable from the selected Gate scope.");
|
|
1658
|
-
}
|
|
1659
|
-
try {
|
|
1660
|
-
const parsed = JSON.parse(raw);
|
|
1661
|
-
if (!parsed || typeof parsed !== "object" || !("operators" in parsed) || !Array.isArray(parsed.operators)) {
|
|
1662
|
-
throw new Error();
|
|
1663
|
-
}
|
|
1664
|
-
const matches = parsed.operators.filter(
|
|
1665
|
-
(entry2) => entry2 && typeof entry2 === "object" && "operatorId" in entry2 && entry2.operatorId === input.operatorId
|
|
1666
|
-
);
|
|
1667
|
-
if (matches.length !== 1) throw new Error();
|
|
1668
|
-
const entry = matches[0];
|
|
1669
|
-
if (typeof entry.loginUsername !== "string" || !entry.loginUsername.trim() || typeof entry.loginPassword !== "string" || !entry.loginPassword.trim()) throw new Error();
|
|
1670
|
-
return { username: entry.loginUsername.trim(), password: entry.loginPassword };
|
|
1671
|
-
} catch {
|
|
1672
|
-
throw new SmsReadError("invalid_operator_directory", "The selected Gate Operator Directory is invalid or does not contain exactly one configured Operator.");
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
// src/operator-actions.ts
|
|
1677
|
-
function profileTarget(target) {
|
|
1678
|
-
return {
|
|
1679
|
-
environment: target.environment,
|
|
1680
|
-
origin: target.origin,
|
|
1681
|
-
operatorId: target.operatorId
|
|
1682
|
-
};
|
|
1683
|
-
}
|
|
1684
|
-
function responseMetadata(api) {
|
|
1685
|
-
const metadata = api.responseMetadata();
|
|
1686
|
-
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
1687
|
-
}
|
|
1688
|
-
function combineSignals(...signals) {
|
|
1689
|
-
const present = signals.filter((signal) => signal !== void 0);
|
|
1690
|
-
if (present.length === 1) return present[0];
|
|
1691
|
-
return AbortSignal.any(present);
|
|
1692
|
-
}
|
|
1693
|
-
function isTransientGateReachabilityError(error) {
|
|
1694
|
-
if (!(error instanceof Error)) return false;
|
|
1695
|
-
return /Could not reach the selected Live Market Terminal Origin/.test(error.message) || /Broker is unreachable/.test(error.message) || /Gate health returned HTTP 5\d\d/.test(error.message);
|
|
1696
|
-
}
|
|
1697
|
-
function waitForSignal(operation, signal) {
|
|
1698
|
-
return new Promise((resolve, reject) => {
|
|
1699
|
-
if (signal.aborted) {
|
|
1700
|
-
reject(new Error("The action exceeded its deadline."));
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
const onAbort = () => reject(new Error("The action exceeded its deadline."));
|
|
1704
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1705
|
-
operation.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
|
-
function createOperatorSessionActions(options) {
|
|
1709
|
-
const selectedTarget = profileTarget(options.target);
|
|
1710
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1711
|
-
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
1712
|
-
let api;
|
|
1713
|
-
async function loadApi() {
|
|
1714
|
-
if (api) return api;
|
|
1715
|
-
const cookies = await options.store.getSession(options.profile, selectedTarget);
|
|
1716
|
-
api = createGateApi(resolveCliApiOrigin(options.target), fetchImpl, {
|
|
1717
|
-
profile: options.profile,
|
|
1718
|
-
...cookies ? { cookies } : {},
|
|
1719
|
-
timeoutMs,
|
|
1720
|
-
...options.signal ? { signal: options.signal } : {}
|
|
1721
|
-
});
|
|
1722
|
-
return api;
|
|
1723
|
-
}
|
|
1724
|
-
async function persistCurrentCookies(client, signal) {
|
|
1725
|
-
const write = options.store.saveSession(options.profile, selectedTarget, client.getCookies());
|
|
1726
|
-
if (signal) await waitForSignal(write, signal);
|
|
1727
|
-
else await write;
|
|
1728
|
-
}
|
|
1729
|
-
async function withAction(action, requestedSignal) {
|
|
1730
|
-
const deadlineAt = Date.now() + timeoutMs;
|
|
1731
|
-
const signal = combineSignals(options.signal, requestedSignal, AbortSignal.timeout(timeoutMs));
|
|
1732
|
-
const client = await waitForSignal(loadApi(), signal);
|
|
1733
|
-
client.setActionSignal(signal);
|
|
1734
|
-
try {
|
|
1735
|
-
return await waitForSignal(action(client, signal, deadlineAt), signal);
|
|
1736
|
-
} finally {
|
|
1737
|
-
client.setActionSignal(options.signal);
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
async function resumeSessionWithClient(client, signal) {
|
|
1741
|
-
try {
|
|
1742
|
-
const observation = await waitForSignal(
|
|
1743
|
-
client.confirmOperatorSessionPresent(options.target.operatorId),
|
|
1744
|
-
signal
|
|
1745
|
-
);
|
|
1746
|
-
await persistCurrentCookies(client, signal);
|
|
1747
|
-
return {
|
|
1748
|
-
observedOperator: observation.operatorId,
|
|
1749
|
-
...observation.clientId ? { clientId: observation.clientId } : {},
|
|
1750
|
-
...responseMetadata(client) ? { metadata: responseMetadata(client) } : {}
|
|
1751
|
-
};
|
|
1752
|
-
} catch (error) {
|
|
1753
|
-
if (!signal.aborted && !client.getCookies().operatorSessionCookie) {
|
|
1754
|
-
await persistCurrentCookies(client, signal).catch(() => void 0);
|
|
1755
|
-
}
|
|
1756
|
-
throw error;
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
async function statusWithClient(client, signal) {
|
|
1760
|
-
try {
|
|
1761
|
-
const observation = await waitForSignal(client.status(options.target.operatorId), signal);
|
|
1762
|
-
await persistCurrentCookies(client, signal);
|
|
1763
|
-
return {
|
|
1764
|
-
observedOperator: observation.operatorId,
|
|
1765
|
-
...observation.clientId ? { clientId: observation.clientId } : {},
|
|
1766
|
-
...responseMetadata(client) ? { metadata: responseMetadata(client) } : {}
|
|
1767
|
-
};
|
|
1768
|
-
} catch (error) {
|
|
1769
|
-
if (!signal.aborted && !client.getCookies().operatorSessionCookie) {
|
|
1770
|
-
await persistCurrentCookies(client, signal).catch(() => void 0);
|
|
1771
|
-
}
|
|
1772
|
-
throw error;
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
async function tryRenewOperatorActivity(client, signal) {
|
|
1776
|
-
try {
|
|
1777
|
-
await waitForSignal(client.reportOperatorActivity(), signal);
|
|
1778
|
-
await persistCurrentCookies(client, signal);
|
|
1779
|
-
} catch (error) {
|
|
1780
|
-
if (error instanceof OperatorSessionRejectedError) throw error;
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
async function loginWithClient(client, username, password, signal) {
|
|
1784
|
-
try {
|
|
1785
|
-
await waitForSignal(client.unlock(username, password, signal), signal);
|
|
1786
|
-
await persistCurrentCookies(client, signal);
|
|
1787
|
-
const result = await statusWithClient(client, signal);
|
|
1788
|
-
await tryRenewOperatorActivity(client, signal);
|
|
1789
|
-
return result;
|
|
1790
|
-
} catch (error) {
|
|
1791
|
-
if (!signal.aborted) await persistCurrentCookies(client, signal).catch(() => void 0);
|
|
1792
|
-
throw error;
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
return {
|
|
1796
|
-
getApi() {
|
|
1797
|
-
return loadApi();
|
|
1798
|
-
},
|
|
1799
|
-
get api() {
|
|
1800
|
-
if (!api) throw new Error("Operator client has not been initialized.");
|
|
1801
|
-
return api;
|
|
1802
|
-
},
|
|
1803
|
-
loginWithPassword(username, password, requestedSignal) {
|
|
1804
|
-
return withAction((client, signal) => loginWithClient(client, username, password, signal), requestedSignal);
|
|
1805
|
-
},
|
|
1806
|
-
loginFromSms() {
|
|
1807
|
-
return withAction(async (client, signal, deadlineAt) => {
|
|
1808
|
-
const smsCredential = await waitForSignal(options.store.getSmsCredential(), signal);
|
|
1809
|
-
if (!smsCredential) throw new SmsReadError("sms_credential_missing", "No protected SMS credential has been provisioned.");
|
|
1810
|
-
const credentials = await waitForSignal(
|
|
1811
|
-
(options.readLoginCredentials ?? readOperatorLoginCredentials)({
|
|
1812
|
-
operatorId: options.target.operatorId,
|
|
1813
|
-
gateScope: options.target.gateScope,
|
|
1814
|
-
smsEndpoint: options.target.smsEndpoint,
|
|
1815
|
-
smsCredential,
|
|
1816
|
-
timeoutMs: Math.max(1, deadlineAt - Date.now()),
|
|
1817
|
-
signal,
|
|
1818
|
-
...options.secretReader ? { secretReader: options.secretReader } : {}
|
|
1819
|
-
}),
|
|
1820
|
-
signal
|
|
1821
|
-
);
|
|
1822
|
-
return loginWithClient(client, credentials.username, credentials.password, signal);
|
|
1823
|
-
});
|
|
1824
|
-
},
|
|
1825
|
-
async resumeInteractiveSession() {
|
|
1826
|
-
const saved = await options.store.getSession(options.profile, selectedTarget);
|
|
1827
|
-
if (!saved?.operatorSessionCookie) return { step: "username" };
|
|
1828
|
-
const maxAttempts = 8;
|
|
1829
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1830
|
-
try {
|
|
1831
|
-
await withAction(async (client, signal) => {
|
|
1832
|
-
await resumeSessionWithClient(client, signal);
|
|
1833
|
-
await tryRenewOperatorActivity(client, signal);
|
|
1834
|
-
});
|
|
1835
|
-
return { step: "menu" };
|
|
1836
|
-
} catch (error) {
|
|
1837
|
-
const retryable = isTransientGateReachabilityError(error) && attempt < maxAttempts - 1;
|
|
1838
|
-
if (retryable) {
|
|
1839
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1840
|
-
continue;
|
|
1841
|
-
}
|
|
1842
|
-
const hint = error instanceof OperatorSessionRejectedError ? error.message : error instanceof Error ? error.message : void 0;
|
|
1843
|
-
return { step: "username", ...hint ? { hint } : {} };
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
return { step: "username" };
|
|
1847
|
-
},
|
|
1848
|
-
reportOperatorActivity(requestedSignal) {
|
|
1849
|
-
return withAction((client, signal) => tryRenewOperatorActivity(client, signal), requestedSignal).then(() => void 0);
|
|
1850
|
-
},
|
|
1851
|
-
status() {
|
|
1852
|
-
return withAction((client, signal) => statusWithClient(client, signal));
|
|
1853
|
-
},
|
|
1854
|
-
statusWithBrokerProof() {
|
|
1855
|
-
return withAction((client, signal) => statusWithClient(client, signal));
|
|
1856
|
-
},
|
|
1857
|
-
logout() {
|
|
1858
|
-
return withAction(async (client, signal) => {
|
|
1859
|
-
await waitForSignal(client.logout(), signal);
|
|
1860
|
-
await waitForSignal(options.store.removeTarget(options.profile, selectedTarget), signal);
|
|
1861
|
-
});
|
|
1862
|
-
}
|
|
1863
|
-
};
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
// src/profile-store.ts
|
|
1867
|
-
init_profile_name();
|
|
1868
|
-
import { randomUUID } from "node:crypto";
|
|
1869
|
-
import {
|
|
1870
|
-
chmod,
|
|
1871
|
-
constants,
|
|
1872
|
-
lstat,
|
|
1873
|
-
mkdir,
|
|
1874
|
-
open,
|
|
1875
|
-
rename,
|
|
1876
|
-
unlink
|
|
1877
|
-
} from "node:fs/promises";
|
|
1878
|
-
import path2 from "node:path";
|
|
1879
|
-
var FILE_NAME = "secrets.pem";
|
|
1880
|
-
var OWNER_ONLY_DIRECTORY = 448;
|
|
1881
|
-
var OWNER_ONLY_FILE = 384;
|
|
1882
|
-
var LOCK_WAIT_MS = 5e3;
|
|
1883
|
-
var LOCK_RETRY_MS = 20;
|
|
1884
|
-
function emptyRecord() {
|
|
1885
|
-
return { version: 2, profiles: {} };
|
|
1886
|
-
}
|
|
1887
|
-
function targetsEqual(left, right) {
|
|
1888
|
-
return left.environment === right.environment && left.origin === right.origin && left.operatorId === right.operatorId;
|
|
1889
|
-
}
|
|
1890
|
-
function assertCookie(value, name) {
|
|
1891
|
-
if (value !== void 0 && !new RegExp(`^${name}=[A-Za-z0-9._~-]+$`).test(value)) {
|
|
1892
|
-
throw new Error(`The ${name} cookie is not valid.`);
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
function ownerId() {
|
|
1896
|
-
return typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
1897
|
-
}
|
|
1898
|
-
async function ensureOwnerOnly(pathname, kind) {
|
|
1899
|
-
let info;
|
|
1900
|
-
try {
|
|
1901
|
-
info = await lstat(pathname);
|
|
1902
|
-
} catch (error) {
|
|
1903
|
-
if (error.code === "ENOENT") throw error;
|
|
1904
|
-
throw new Error("Could not inspect the protected CLI profile storage.");
|
|
1905
|
-
}
|
|
1906
|
-
if (info.isSymbolicLink() || (kind === "directory" ? !info.isDirectory() : !info.isFile())) {
|
|
1907
|
-
throw new Error(`The protected CLI profile ${kind} must be a regular ${kind}.`);
|
|
1908
|
-
}
|
|
1909
|
-
const uid = ownerId();
|
|
1910
|
-
if (uid !== void 0 && info.uid !== uid) {
|
|
1911
|
-
throw new Error(`The protected CLI profile ${kind} must be owned by the current user.`);
|
|
1912
|
-
}
|
|
1913
|
-
const expectedMode = kind === "directory" ? OWNER_ONLY_DIRECTORY : OWNER_ONLY_FILE;
|
|
1914
|
-
if ((info.mode & 511) === expectedMode) return;
|
|
1915
|
-
try {
|
|
1916
|
-
await chmod(pathname, expectedMode);
|
|
1917
|
-
} catch {
|
|
1918
|
-
throw new Error(`Could not set protected CLI profile ${kind} permissions.`);
|
|
1919
|
-
}
|
|
1920
|
-
const updated = await lstat(pathname);
|
|
1921
|
-
if ((updated.mode & 511) !== expectedMode) {
|
|
1922
|
-
const mode = expectedMode.toString(8).padStart(4, "0");
|
|
1923
|
-
throw new Error(`The protected CLI profile ${kind} must have mode ${mode}.`);
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
function parseLegacyTargetKey(key) {
|
|
1927
|
-
try {
|
|
1928
|
-
const parsed = JSON.parse(key);
|
|
1929
|
-
if (!Array.isArray(parsed) || parsed.length !== 3) return void 0;
|
|
1930
|
-
const [environment, origin, operatorId] = parsed;
|
|
1931
|
-
if (environment !== "local" && environment !== "dev") return void 0;
|
|
1932
|
-
if (typeof origin !== "string" || !origin.trim()) return void 0;
|
|
1933
|
-
if (operatorId !== "OPERATOR_1") return void 0;
|
|
1934
|
-
return { environment, origin: origin.trim(), operatorId };
|
|
1935
|
-
} catch {
|
|
1936
|
-
return void 0;
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
function migrateLegacyRecord(record) {
|
|
1940
|
-
const migrated = emptyRecord();
|
|
1941
|
-
if (record.smsCredential?.trim()) migrated.smsCredential = record.smsCredential;
|
|
1942
|
-
for (const [profileName, profileRecord] of Object.entries(record.profiles ?? {})) {
|
|
1943
|
-
const sessions = [];
|
|
1944
|
-
for (const [targetKey, stored] of Object.entries(profileRecord.targets ?? {})) {
|
|
1945
|
-
const target = parseLegacyTargetKey(targetKey);
|
|
1946
|
-
if (!target) continue;
|
|
1947
|
-
sessions.push({
|
|
1948
|
-
...target,
|
|
1949
|
-
...stored.operatorSessionCookie ? { operatorSessionCookie: stored.operatorSessionCookie } : {},
|
|
1950
|
-
...stored.clientIdentityCookie ? { clientIdentityCookie: stored.clientIdentityCookie } : {},
|
|
1951
|
-
updatedAt: stored.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1952
|
-
});
|
|
1953
|
-
}
|
|
1954
|
-
if (sessions.length > 0) migrated.profiles[profileName] = { sessions };
|
|
1955
|
-
}
|
|
1956
|
-
return migrated;
|
|
1957
|
-
}
|
|
1958
|
-
function normalizeSession(value) {
|
|
1959
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1960
|
-
const session = value;
|
|
1961
|
-
if (session.environment !== "local" && session.environment !== "dev") return void 0;
|
|
1962
|
-
if (typeof session.origin !== "string" || !session.origin.trim()) return void 0;
|
|
1963
|
-
if (session.operatorId !== "OPERATOR_1") return void 0;
|
|
1964
|
-
if (typeof session.updatedAt !== "string" || !session.updatedAt.trim()) return void 0;
|
|
1965
|
-
if (session.operatorSessionCookie !== void 0 && typeof session.operatorSessionCookie !== "string") return void 0;
|
|
1966
|
-
if (session.clientIdentityCookie !== void 0 && typeof session.clientIdentityCookie !== "string") return void 0;
|
|
1967
|
-
return {
|
|
1968
|
-
environment: session.environment,
|
|
1969
|
-
origin: session.origin.trim(),
|
|
1970
|
-
operatorId: session.operatorId,
|
|
1971
|
-
updatedAt: session.updatedAt,
|
|
1972
|
-
...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
|
|
1973
|
-
...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {}
|
|
1974
|
-
};
|
|
1975
|
-
}
|
|
1976
|
-
function parseRecord(raw) {
|
|
1977
|
-
let parsed;
|
|
1978
|
-
try {
|
|
1979
|
-
parsed = JSON.parse(raw);
|
|
1980
|
-
} catch {
|
|
1981
|
-
throw new Error("The protected CLI profile file is not valid JSON.");
|
|
1982
|
-
}
|
|
1983
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1984
|
-
throw new Error("The protected CLI profile file has an unsupported format.");
|
|
1985
|
-
}
|
|
1986
|
-
const record = parsed;
|
|
1987
|
-
if (record.version === 1) {
|
|
1988
|
-
return migrateLegacyRecord(parsed);
|
|
1989
|
-
}
|
|
1990
|
-
if (record.version !== 2 || !record.profiles || typeof record.profiles !== "object" || Array.isArray(record.profiles)) {
|
|
1991
|
-
throw new Error("The protected CLI profile file has an unsupported version or format.");
|
|
1992
|
-
}
|
|
1993
|
-
if (record.smsCredential !== void 0 && (typeof record.smsCredential !== "string" || !record.smsCredential.trim())) {
|
|
1994
|
-
throw new Error("The protected CLI profile file has an invalid SMS credential record.");
|
|
1995
|
-
}
|
|
1996
|
-
const profiles = {};
|
|
1997
|
-
for (const [profileName, profileValue] of Object.entries(record.profiles)) {
|
|
1998
|
-
if (!profileValue || typeof profileValue !== "object" || Array.isArray(profileValue)) {
|
|
1999
|
-
throw new Error("The protected CLI profile file has an unsupported version or format.");
|
|
2000
|
-
}
|
|
2001
|
-
const sessionsValue = profileValue.sessions;
|
|
2002
|
-
if (!Array.isArray(sessionsValue)) {
|
|
2003
|
-
throw new Error("The protected CLI profile file has an unsupported version or format.");
|
|
2004
|
-
}
|
|
2005
|
-
const sessions = sessionsValue.map(normalizeSession).filter((session) => session !== void 0);
|
|
2006
|
-
if (sessions.length !== sessionsValue.length) {
|
|
2007
|
-
throw new Error("The protected CLI profile file has an unsupported version or format.");
|
|
2008
|
-
}
|
|
2009
|
-
if (sessions.length > 0) profiles[profileName] = { sessions };
|
|
2010
|
-
}
|
|
2011
|
-
return {
|
|
2012
|
-
version: 2,
|
|
2013
|
-
...typeof record.smsCredential === "string" && record.smsCredential.trim() ? { smsCredential: record.smsCredential } : {},
|
|
2014
|
-
profiles
|
|
2015
|
-
};
|
|
2016
|
-
}
|
|
2017
|
-
function findSessionIndex(sessions, target) {
|
|
2018
|
-
return sessions.findIndex((session) => targetsEqual(session, target));
|
|
2019
|
-
}
|
|
2020
|
-
function sessionHasCookies(session) {
|
|
2021
|
-
return Boolean(session.operatorSessionCookie || session.clientIdentityCookie);
|
|
2022
|
-
}
|
|
2023
|
-
var CliProfileStore = class {
|
|
2024
|
-
constructor(homeDirectory = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd(), lockWaitMs = LOCK_WAIT_MS, directoryName = ".lmt") {
|
|
2025
|
-
this.lockWaitMs = lockWaitMs;
|
|
2026
|
-
this.directory = path2.join(homeDirectory, directoryName);
|
|
2027
|
-
this.filePath = path2.join(this.directory, FILE_NAME);
|
|
2028
|
-
this.lockPath = path2.join(this.directory, `${FILE_NAME}.lock`);
|
|
2029
|
-
}
|
|
2030
|
-
lockWaitMs;
|
|
2031
|
-
directory;
|
|
2032
|
-
filePath;
|
|
2033
|
-
lockPath;
|
|
2034
|
-
async getSmsCredential() {
|
|
2035
|
-
return this.withLock(async () => (await this.readRecord()).smsCredential);
|
|
2036
|
-
}
|
|
2037
|
-
async saveSmsCredential(credential) {
|
|
2038
|
-
if (!credential.trim() || /[\r\n]/.test(credential)) throw new Error("SMS credential input must be a non-empty single line.");
|
|
2039
|
-
await this.withLock(async () => {
|
|
2040
|
-
const record = await this.readRecordForSmsCredentialUpdate();
|
|
2041
|
-
record.smsCredential = credential;
|
|
2042
|
-
await this.writeRecord(record);
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2045
|
-
async getSession(profile, target) {
|
|
2046
|
-
assertClientProfileName(profile);
|
|
2047
|
-
return this.withLock(async () => {
|
|
2048
|
-
const stored = (await this.readRecord()).profiles[profile]?.sessions.find((session) => targetsEqual(session, target));
|
|
2049
|
-
if (!stored) return void 0;
|
|
2050
|
-
return {
|
|
2051
|
-
...stored.operatorSessionCookie ? { operatorSessionCookie: stored.operatorSessionCookie } : {},
|
|
2052
|
-
...stored.clientIdentityCookie ? { clientIdentityCookie: stored.clientIdentityCookie } : {}
|
|
2053
|
-
};
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
|
-
async saveSession(profile, target, cookies) {
|
|
2057
|
-
assertClientProfileName(profile);
|
|
2058
|
-
assertCookie(cookies.operatorSessionCookie, "lmt_operator_session");
|
|
2059
|
-
assertCookie(cookies.clientIdentityCookie, "lmt_client_id");
|
|
2060
|
-
if (!cookies.operatorSessionCookie && !cookies.clientIdentityCookie) return;
|
|
2061
|
-
await this.update((record) => {
|
|
2062
|
-
const profileRecord = record.profiles[profile] ?? { sessions: [] };
|
|
2063
|
-
const index = findSessionIndex(profileRecord.sessions, target);
|
|
2064
|
-
const next = {
|
|
2065
|
-
...target,
|
|
2066
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2067
|
-
...cookies.clientIdentityCookie ? { clientIdentityCookie: cookies.clientIdentityCookie } : {},
|
|
2068
|
-
...cookies.operatorSessionCookie ? { operatorSessionCookie: cookies.operatorSessionCookie } : {}
|
|
2069
|
-
};
|
|
2070
|
-
if (!sessionHasCookies(next)) {
|
|
2071
|
-
if (index >= 0) profileRecord.sessions.splice(index, 1);
|
|
2072
|
-
} else if (index >= 0) {
|
|
2073
|
-
profileRecord.sessions[index] = next;
|
|
2074
|
-
} else {
|
|
2075
|
-
profileRecord.sessions.push(next);
|
|
2076
|
-
}
|
|
2077
|
-
if (profileRecord.sessions.length === 0) delete record.profiles[profile];
|
|
2078
|
-
else record.profiles[profile] = profileRecord;
|
|
2079
|
-
});
|
|
2080
|
-
}
|
|
2081
|
-
async clearOperatorSession(profile, target) {
|
|
2082
|
-
assertClientProfileName(profile);
|
|
2083
|
-
await this.update((record) => {
|
|
2084
|
-
const profileRecord = record.profiles[profile];
|
|
2085
|
-
if (!profileRecord) return;
|
|
2086
|
-
const index = findSessionIndex(profileRecord.sessions, target);
|
|
2087
|
-
if (index < 0) return;
|
|
2088
|
-
const stored = profileRecord.sessions[index];
|
|
2089
|
-
delete stored.operatorSessionCookie;
|
|
2090
|
-
stored.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2091
|
-
this.cleanEmptySession(profileRecord, index);
|
|
2092
|
-
if (profileRecord.sessions.length === 0) delete record.profiles[profile];
|
|
2093
|
-
});
|
|
2094
|
-
}
|
|
2095
|
-
async removeTarget(profile, target) {
|
|
2096
|
-
assertClientProfileName(profile);
|
|
2097
|
-
await this.update((record) => {
|
|
2098
|
-
const profileRecord = record.profiles[profile];
|
|
2099
|
-
if (!profileRecord) return;
|
|
2100
|
-
profileRecord.sessions = profileRecord.sessions.filter((session) => !targetsEqual(session, target));
|
|
2101
|
-
if (profileRecord.sessions.length === 0) delete record.profiles[profile];
|
|
2102
|
-
});
|
|
2103
|
-
}
|
|
2104
|
-
cleanEmptySession(profileRecord, index) {
|
|
2105
|
-
const stored = profileRecord.sessions[index];
|
|
2106
|
-
if (stored && !sessionHasCookies(stored)) profileRecord.sessions.splice(index, 1);
|
|
2107
|
-
}
|
|
2108
|
-
async update(mutator) {
|
|
2109
|
-
await this.withLock(async () => {
|
|
2110
|
-
const record = await this.readRecord();
|
|
2111
|
-
mutator(record);
|
|
2112
|
-
await this.writeRecord(record);
|
|
2113
|
-
});
|
|
2114
|
-
}
|
|
2115
|
-
async withLock(operation) {
|
|
2116
|
-
await this.ensureDirectory();
|
|
2117
|
-
const started = Date.now();
|
|
2118
|
-
let lock;
|
|
2119
|
-
while (!lock) {
|
|
2120
|
-
try {
|
|
2121
|
-
lock = await open(this.lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), OWNER_ONLY_FILE);
|
|
2122
|
-
await lock.chmod(OWNER_ONLY_FILE);
|
|
2123
|
-
await lock.writeFile(`${process.pid}
|
|
2124
|
-
`);
|
|
2125
|
-
await lock.sync();
|
|
2126
|
-
} catch (error) {
|
|
2127
|
-
if (error.code !== "EEXIST") {
|
|
2128
|
-
throw new Error("Could not acquire the protected CLI profile storage lock.");
|
|
2129
|
-
}
|
|
2130
|
-
try {
|
|
2131
|
-
await ensureOwnerOnly(this.lockPath, "file");
|
|
2132
|
-
} catch (lockError) {
|
|
2133
|
-
if (lockError.code === "ENOENT") continue;
|
|
2134
|
-
throw lockError;
|
|
2135
|
-
}
|
|
2136
|
-
if (Date.now() - started >= this.lockWaitMs) {
|
|
2137
|
-
throw new Error("Timed out waiting for the protected CLI profile storage lock.");
|
|
2138
|
-
}
|
|
2139
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
try {
|
|
2143
|
-
return await operation();
|
|
2144
|
-
} finally {
|
|
2145
|
-
await lock.close().catch(() => void 0);
|
|
2146
|
-
await unlink(this.lockPath).catch(() => void 0);
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
async ensureDirectory() {
|
|
2150
|
-
let created = false;
|
|
2151
|
-
try {
|
|
2152
|
-
await mkdir(this.directory, { mode: OWNER_ONLY_DIRECTORY });
|
|
2153
|
-
created = true;
|
|
2154
|
-
} catch (error) {
|
|
2155
|
-
if (error.code !== "EEXIST") {
|
|
2156
|
-
throw new Error("Could not create the protected CLI profile directory.");
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
if (created) {
|
|
2160
|
-
try {
|
|
2161
|
-
await chmod(this.directory, OWNER_ONLY_DIRECTORY);
|
|
2162
|
-
} catch {
|
|
2163
|
-
throw new Error("Could not set protected CLI profile directory permissions.");
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
await ensureOwnerOnly(this.directory, "directory");
|
|
2167
|
-
}
|
|
2168
|
-
isRecoverableStoreFormatError(error) {
|
|
2169
|
-
if (!(error instanceof Error)) return false;
|
|
2170
|
-
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.";
|
|
2171
|
-
}
|
|
2172
|
-
async archiveUnreadableStoreFile() {
|
|
2173
|
-
const backupPath = `${this.filePath}.corrupt.${Date.now()}.bak`;
|
|
2174
|
-
try {
|
|
2175
|
-
await rename(this.filePath, backupPath);
|
|
2176
|
-
} catch {
|
|
2177
|
-
throw new Error("Could not repair the protected CLI profile file.");
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
async readRecordForSmsCredentialUpdate() {
|
|
2181
|
-
try {
|
|
2182
|
-
return await this.readRecord();
|
|
2183
|
-
} catch (error) {
|
|
2184
|
-
if (!this.isRecoverableStoreFormatError(error)) throw error;
|
|
2185
|
-
await this.archiveUnreadableStoreFile();
|
|
2186
|
-
return emptyRecord();
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
async readRecord() {
|
|
2190
|
-
try {
|
|
2191
|
-
const info = await lstat(this.filePath);
|
|
2192
|
-
if (info.isSymbolicLink() || !info.isFile()) {
|
|
2193
|
-
throw new Error("The protected CLI profile file must be a regular file.");
|
|
2194
|
-
}
|
|
2195
|
-
await ensureOwnerOnly(this.filePath, "file");
|
|
2196
|
-
const handle = await open(this.filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2197
|
-
try {
|
|
2198
|
-
return parseRecord(await handle.readFile("utf8"));
|
|
2199
|
-
} finally {
|
|
2200
|
-
await handle.close();
|
|
2201
|
-
}
|
|
2202
|
-
} catch (error) {
|
|
2203
|
-
if (error.code === "ENOENT") return emptyRecord();
|
|
2204
|
-
if (error instanceof Error && error.message.startsWith("The protected CLI profile")) throw error;
|
|
2205
|
-
throw new Error("Could not read the protected CLI profile file.");
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
async writeRecord(record) {
|
|
2209
|
-
const normalized = {
|
|
2210
|
-
version: 2,
|
|
2211
|
-
...record.smsCredential ? { smsCredential: record.smsCredential } : {},
|
|
2212
|
-
profiles: Object.fromEntries(
|
|
2213
|
-
Object.entries(record.profiles).map(([profileName, profileRecord]) => [
|
|
2214
|
-
profileName,
|
|
2215
|
-
{
|
|
2216
|
-
sessions: profileRecord.sessions.map((session) => ({
|
|
2217
|
-
environment: session.environment,
|
|
2218
|
-
origin: session.origin,
|
|
2219
|
-
operatorId: session.operatorId,
|
|
2220
|
-
...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
|
|
2221
|
-
...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {},
|
|
2222
|
-
updatedAt: session.updatedAt
|
|
2223
|
-
}))
|
|
2224
|
-
}
|
|
2225
|
-
])
|
|
2226
|
-
)
|
|
2227
|
-
};
|
|
2228
|
-
const temporaryPath = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
2229
|
-
const content = `${JSON.stringify(normalized, null, 2)}
|
|
2230
|
-
`;
|
|
2231
|
-
let handle;
|
|
2232
|
-
try {
|
|
2233
|
-
handle = await open(temporaryPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), OWNER_ONLY_FILE);
|
|
2234
|
-
await handle.chmod(OWNER_ONLY_FILE);
|
|
2235
|
-
await handle.writeFile(content, "utf8");
|
|
2236
|
-
await handle.sync();
|
|
2237
|
-
await handle.close();
|
|
2238
|
-
handle = void 0;
|
|
2239
|
-
await rename(temporaryPath, this.filePath);
|
|
2240
|
-
const directoryHandle = await open(this.directory, constants.O_RDONLY);
|
|
2241
|
-
try {
|
|
2242
|
-
await directoryHandle.sync();
|
|
2243
|
-
} finally {
|
|
2244
|
-
await directoryHandle.close();
|
|
2245
|
-
}
|
|
2246
|
-
} catch {
|
|
2247
|
-
if (handle) await handle.close().catch(() => void 0);
|
|
2248
|
-
await unlink(temporaryPath).catch(() => void 0);
|
|
2249
|
-
throw new Error("Could not atomically update the protected CLI profile file.");
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
};
|
|
2253
|
-
|
|
2254
|
-
// src/zerodha-validation.ts
|
|
2255
|
-
init_action_deadline();
|
|
2256
|
-
init_gate_api();
|
|
2257
|
-
init_profile_name();
|
|
2258
|
-
|
|
2259
|
-
// src/protected-input.ts
|
|
2260
|
-
var ProtectedInputError = class extends Error {
|
|
2261
|
-
code = "invalid_invocation";
|
|
2262
|
-
};
|
|
2263
|
-
async function readProtectedOtp(stdin, signal) {
|
|
2264
|
-
if (stdin.isTTY) throw new ProtectedInputError("OTP requires piped protected stdin; interactive mode provides masked entry.");
|
|
2265
|
-
const chunks = [];
|
|
2266
|
-
let size = 0;
|
|
2267
|
-
try {
|
|
2268
|
-
const iterator = stdin[Symbol.asyncIterator]();
|
|
2269
|
-
while (true) {
|
|
2270
|
-
if (signal.aborted) throw new Error("The action exceeded its deadline.");
|
|
2271
|
-
const next = await new Promise((resolve, reject) => {
|
|
2272
|
-
const abort = () => {
|
|
2273
|
-
stdin.destroy?.();
|
|
2274
|
-
reject(new Error("The action exceeded its deadline."));
|
|
2275
|
-
};
|
|
2276
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
2277
|
-
Promise.resolve(iterator.next()).then(resolve, () => reject(new ProtectedInputError("Could not read OTP from protected stdin; provide one six-digit line."))).finally(() => signal.removeEventListener("abort", abort));
|
|
2278
|
-
});
|
|
2279
|
-
if (next.done) break;
|
|
2280
|
-
size += typeof next.value === "string" ? Buffer.byteLength(next.value) : next.value.byteLength;
|
|
2281
|
-
if (size > 8) {
|
|
2282
|
-
stdin.destroy?.();
|
|
2283
|
-
throw new ProtectedInputError("OTP input is too large; provide one six-digit line.");
|
|
2284
|
-
}
|
|
2285
|
-
chunks.push(Buffer.from(next.value));
|
|
2286
|
-
}
|
|
2287
|
-
const buffer = Buffer.concat(chunks);
|
|
2288
|
-
try {
|
|
2289
|
-
const value = buffer.toString("utf8");
|
|
2290
|
-
if (!/^\d{6}(?:\r?\n)?$/.test(value)) throw new ProtectedInputError("OTP input must be one six-digit line.");
|
|
2291
|
-
return value.slice(0, 6);
|
|
2292
|
-
} finally {
|
|
2293
|
-
buffer.fill(0);
|
|
2294
|
-
}
|
|
2295
|
-
} catch (error) {
|
|
2296
|
-
if (signal.aborted || error instanceof ProtectedInputError) throw error;
|
|
2297
|
-
throw new ProtectedInputError("Could not read OTP from protected stdin; provide one six-digit line.");
|
|
2298
|
-
} finally {
|
|
2299
|
-
for (const chunk of chunks) chunk.fill(0);
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
// src/zerodha-validation.ts
|
|
2304
|
-
init_zerodha_actions();
|
|
2305
|
-
async function runZerodhaValidation(options) {
|
|
2306
|
-
const started = performance.now();
|
|
2307
|
-
const controller = new AbortController();
|
|
2308
|
-
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 12e4);
|
|
2309
|
-
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
2310
|
-
const observations = [];
|
|
2311
|
-
let api;
|
|
2312
|
-
let otp;
|
|
2313
|
-
let result = {
|
|
2314
|
-
scenario: "zerodha-session",
|
|
2315
|
-
outcome: "fail",
|
|
2316
|
-
reason: "validation_action_failed",
|
|
2317
|
-
exitCode: 1,
|
|
2318
|
-
target: { environment: options.target.environment, origin: options.target.origin },
|
|
2319
|
-
profile: options.callerProfile,
|
|
2320
|
-
...options.attemptId ? { attemptId: options.attemptId } : {},
|
|
2321
|
-
observations,
|
|
2322
|
-
nextStep: null
|
|
2323
|
-
};
|
|
2324
|
-
const safeText = (message) => (otp ? message.replaceAll(otp, "[REDACTED]") : message).replace(/lmt_(?:operator_session|client_id)=[^\s,;]+/g, "[REDACTED]").replace(/(password|token|credential|cookie)\s*[=:]\s*[^\s,;]+/gi, "$1=[REDACTED]");
|
|
2325
|
-
const resumeStep = () => result.attemptId ? `Use the same environment and profile: validate zerodha-session --attempt ${result.attemptId} --otp-stdin. Observe without --otp-stdin; start explicitly only if Gate reports the attempt inactive.` : "Run validate zerodha-session again after resolving the reported prerequisite.";
|
|
2326
|
-
try {
|
|
2327
|
-
if (!["local", "dev"].includes(options.target.environment)) throw Object.assign(new Error("The CLI supports local or dev targets only; prod is not permitted."), { code: "invalid_target" });
|
|
2328
|
-
try {
|
|
2329
|
-
assertClientProfileName(options.callerProfile);
|
|
2330
|
-
} catch {
|
|
2331
|
-
throw new ProtectedInputError(CLIENT_PROFILE_NAME_ERROR);
|
|
2332
|
-
}
|
|
2333
|
-
if (options.attemptId !== void 0 && !/^[a-zA-Z0-9_-]{1,200}$/.test(options.attemptId)) throw new ProtectedInputError("--attempt must be a valid Zerodha attempt ID.");
|
|
2334
|
-
if (options.readOtp && !options.attemptId) throw new ProtectedInputError("OTP submission requires an explicit --attempt ID from a prior login or validation.");
|
|
2335
|
-
const actions = createOperatorSessionActions({
|
|
2336
|
-
target: options.target,
|
|
2337
|
-
profile: options.callerProfile,
|
|
2338
|
-
store: options.store,
|
|
2339
|
-
timeoutMs: options.timeoutMs ?? 12e4,
|
|
2340
|
-
signal,
|
|
2341
|
-
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
2342
|
-
});
|
|
2343
|
-
api = await withinActionDeadline(actions.getApi(), signal, "The Zerodha Session validation exceeded its deadline.");
|
|
2344
|
-
const operator = await actions.status();
|
|
2345
|
-
result.observedOperator = operator.observedOperator;
|
|
2346
|
-
if (options.readOtp) {
|
|
2347
|
-
otp = await withinActionDeadline(options.readOtp(signal), signal, "The Zerodha Session validation exceeded its deadline.");
|
|
2348
|
-
if (!/^\d{6}$/.test(otp)) throw new ProtectedInputError("OTP input must be one six-digit line.");
|
|
2349
|
-
}
|
|
2350
|
-
const action = options.attemptId ? options.readOtp ? "otp" : "observe" : "login";
|
|
2351
|
-
const login = await runZerodhaAction(api, action, options.target.operatorId, signal, options.attemptId, otp, {
|
|
2352
|
-
onObservation(event) {
|
|
2353
|
-
result.attemptId = event.attemptId;
|
|
2354
|
-
if (event.type === "status") {
|
|
2355
|
-
result.observedState = event.status;
|
|
2356
|
-
result.observedOperator = event.operatorId;
|
|
2357
|
-
observations.push(event.status);
|
|
2358
|
-
if (event.failedOtpAttempts !== void 0) result.failedOtpAttempts = event.failedOtpAttempts;
|
|
2359
|
-
if (event.message) result.message = safeText(event.message);
|
|
2360
|
-
}
|
|
2361
|
-
options.onObservation?.(event);
|
|
2362
|
-
}
|
|
2363
|
-
});
|
|
2364
|
-
result.attemptId = login.attemptId;
|
|
2365
|
-
if (login.status === "awaiting_otp") {
|
|
2366
|
-
const rejected = !!options.readOtp && (login.failedOtpAttempts ?? 0) > 0;
|
|
2367
|
-
result = { ...result, outcome: rejected ? "fail" : "blocked", reason: rejected ? "otp_rejected" : "awaiting_otp", exitCode: rejected ? 1 : 4, nextStep: resumeStep() };
|
|
2368
|
-
} else {
|
|
2369
|
-
result = { ...result, outcome: "pass", reason: "zerodha_session_validated", exitCode: 0, nextStep: null };
|
|
2370
|
-
}
|
|
2371
|
-
} catch (cause) {
|
|
2372
|
-
const error = cause instanceof Error ? cause : void 0;
|
|
2373
|
-
if (error?.attemptId) result.attemptId = error.attemptId;
|
|
2374
|
-
if (error?.observedState) result.observedState = error.observedState;
|
|
2375
|
-
if (error?.observedOperator) result.observedOperator = error.observedOperator;
|
|
2376
|
-
const code = error?.code;
|
|
2377
|
-
if (signal.aborted) {
|
|
2378
|
-
result.reason = "deadline_exceeded";
|
|
2379
|
-
result.message = "The Zerodha Session validation exceeded its deadline.";
|
|
2380
|
-
} else if (cause instanceof OperatorSessionRejectedError) {
|
|
2381
|
-
result.outcome = "blocked";
|
|
2382
|
-
result.reason = "operator_login_required";
|
|
2383
|
-
result.exitCode = 3;
|
|
2384
|
-
result.message = "No valid Operator Session is available; run login with this environment and profile first.";
|
|
2385
|
-
} else if (code && ["zerodha_credentials_missing", "secret_store_unavailable", "operator_not_configured"].includes(code)) {
|
|
2386
|
-
result.outcome = "blocked";
|
|
2387
|
-
result.reason = code;
|
|
2388
|
-
result.exitCode = 4;
|
|
2389
|
-
result.message = safeText(error.message);
|
|
2390
|
-
} else {
|
|
2391
|
-
result.reason = cause instanceof ProtectedInputError ? "invalid_invocation" : cause instanceof ZerodhaLoginAttemptUnavailableError ? "login_attempt_inactive" : cause instanceof ZerodhaLoginFailedError ? "zerodha_login_failed" : code ?? "validation_action_failed";
|
|
2392
|
-
result.exitCode = cause instanceof ProtectedInputError || code === "invalid_target" ? 2 : 1;
|
|
2393
|
-
result.message = error ? safeText(error.message) : "The Zerodha Session validation failed.";
|
|
2394
|
-
}
|
|
2395
|
-
result.nextStep = result.reason === "operator_login_required" ? "Run login with the selected environment and profile, then explicitly start Zerodha validation." : result.reason === "zerodha_credentials_missing" ? "Provision the selected Operator's Broker-owned Zerodha credentials, then explicitly start validation." : result.reason === "secret_store_unavailable" ? "Restore Broker access to its secret store, then explicitly start validation." : result.reason === "operator_not_configured" ? "Configure the selected Operator on Broker, then explicitly start validation." : resumeStep();
|
|
2396
|
-
if (cause instanceof OperatorSessionRejectedError && api && !signal.aborted) {
|
|
2397
|
-
await options.store.saveSession(options.callerProfile, options.target, api.getCookies()).catch(() => void 0);
|
|
2398
|
-
}
|
|
2399
|
-
} finally {
|
|
2400
|
-
otp = void 0;
|
|
2401
|
-
clearTimeout(timer);
|
|
2402
|
-
}
|
|
2403
|
-
const metadata = api?.responseMetadata();
|
|
2404
|
-
return { ...result, elapsedMs: Math.round(performance.now() - started), requestId: metadata?.requestId ?? null, buildId: metadata?.buildId ?? null };
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
// src/commands.ts
|
|
2408
|
-
init_zerodha_actions();
|
|
2409
|
-
|
|
2410
|
-
// src/pair-commands.ts
|
|
2411
|
-
import { createHash } from "node:crypto";
|
|
2412
|
-
import { writeFile } from "node:fs/promises";
|
|
2413
|
-
import path3 from "node:path";
|
|
2414
|
-
var PairInvocationError = class extends Error {
|
|
2415
|
-
code = "invalid_invocation";
|
|
2416
|
-
};
|
|
2417
|
-
var PAIR_USAGE = "Use pair list, pair assign --ce ROW --pe ROW --lots N, pair unpair PAIR_ID --confirm, or pair report PAIR_ID [--publish | --output PATH].";
|
|
2418
|
-
var VALUE_OPTIONS = /* @__PURE__ */ new Set(["--env", "--profile", "--timeout", "--ce", "--pe", "--lots", "--output"]);
|
|
2419
|
-
var FLAG_OPTIONS = /* @__PURE__ */ new Set(["--confirm", "--publish"]);
|
|
2420
|
-
var PAIR_ID = /^[A-Za-z0-9_-]{1,200}$/;
|
|
2421
|
-
function isPairInvocation(argv) {
|
|
2422
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
2423
|
-
const token = argv[index];
|
|
2424
|
-
if (token === "--env" || token === "--profile" || token === "--timeout") {
|
|
2425
|
-
index += 1;
|
|
2426
|
-
continue;
|
|
2427
|
-
}
|
|
2428
|
-
if (token.startsWith("--")) continue;
|
|
2429
|
-
return token === "pair";
|
|
2430
|
-
}
|
|
2431
|
-
return false;
|
|
2432
|
-
}
|
|
2433
|
-
function parsePairArguments(argv, parseTimeout2, validateProfile) {
|
|
2434
|
-
const options = /* @__PURE__ */ new Map();
|
|
2435
|
-
const flags = /* @__PURE__ */ new Set();
|
|
2436
|
-
const positional = [];
|
|
2437
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
2438
|
-
const token = argv[index];
|
|
2439
|
-
if (VALUE_OPTIONS.has(token)) {
|
|
2440
|
-
const value = argv[index + 1];
|
|
2441
|
-
if (value === void 0 || value.startsWith("--")) throw new PairInvocationError(`Invalid or incomplete command argument: ${token}`);
|
|
2442
|
-
if (options.has(token)) throw new PairInvocationError(`Argument ${token} may be specified only once.`);
|
|
2443
|
-
options.set(token, value);
|
|
2444
|
-
index += 1;
|
|
2445
|
-
} else if (FLAG_OPTIONS.has(token)) {
|
|
2446
|
-
if (flags.has(token)) throw new PairInvocationError(`Argument ${token} may be specified only once.`);
|
|
2447
|
-
flags.add(token);
|
|
2448
|
-
} else if (token.startsWith("--")) {
|
|
2449
|
-
throw new PairInvocationError(`Unsupported command option: ${token.split("=")[0]}`);
|
|
2450
|
-
} else positional.push(token);
|
|
2451
|
-
}
|
|
2452
|
-
const [, action, pairId, ...extra] = positional;
|
|
2453
|
-
const allowed = {
|
|
2454
|
-
list: [],
|
|
2455
|
-
assign: ["--ce", "--pe", "--lots"],
|
|
2456
|
-
unpair: ["--confirm"],
|
|
2457
|
-
report: ["--publish", "--output"]
|
|
2458
|
-
};
|
|
2459
|
-
if (!action || !(action in allowed) || extra.length > 0 || [...options.keys(), ...flags].some((key) => !["--env", "--profile", "--timeout", ...allowed[action]].includes(key))) {
|
|
2460
|
-
throw new PairInvocationError(PAIR_USAGE);
|
|
2461
|
-
}
|
|
2462
|
-
const environment = options.get("--env"), profile = options.get("--profile");
|
|
2463
|
-
if (!environment || !profile) throw new PairInvocationError("Headless commands require both --env local|dev and --profile NAME.");
|
|
2464
|
-
validateProfile(profile);
|
|
2465
|
-
const target = { environment, profile, ...options.has("--timeout") ? { timeoutMs: parseTimeout2(options.get("--timeout")) } : {} };
|
|
2466
|
-
if (action === "list") {
|
|
2467
|
-
if (pairId !== void 0) throw new PairInvocationError(PAIR_USAGE);
|
|
2468
|
-
return { kind: "pair", action, ...target };
|
|
2469
|
-
}
|
|
2470
|
-
if (action === "assign") {
|
|
2471
|
-
if (pairId !== void 0) throw new PairInvocationError(PAIR_USAGE);
|
|
2472
|
-
const ce = options.get("--ce"), pe = options.get("--pe"), lots = options.get("--lots");
|
|
2473
|
-
if (!ce || !pe || !lots || !/^\d{1,3}$/.test(ce) || !/^\d{1,3}$/.test(pe) || !/^-?\d{1,6}$/.test(lots)) {
|
|
2474
|
-
throw new PairInvocationError("pair assign requires --ce ROW --pe ROW --lots N as whole numbers.");
|
|
2475
|
-
}
|
|
2476
|
-
return { kind: "pair", action, ceRow: Number(ce), peRow: Number(pe), pairLots: Number(lots), ...target };
|
|
2477
|
-
}
|
|
2478
|
-
if (!pairId || !PAIR_ID.test(pairId)) throw new PairInvocationError(`pair ${action} requires a Price Cross pair ID from pair list.`);
|
|
2479
|
-
if (action === "unpair") {
|
|
2480
|
-
if (!flags.has("--confirm")) {
|
|
2481
|
-
throw new PairInvocationError("pair unpair requires --confirm. Unpair closes any paper position at its latest complete close.");
|
|
2482
|
-
}
|
|
2483
|
-
return { kind: "pair", action, pairId, ...target };
|
|
2484
|
-
}
|
|
2485
|
-
const publish = flags.has("--publish");
|
|
2486
|
-
if (publish && options.has("--output")) throw new PairInvocationError("--output downloads the workbook; --publish returns a URL. Choose one.");
|
|
2487
|
-
return { kind: "pair", action: "report", pairId, publish, ...options.has("--output") ? { output: options.get("--output") } : {}, ...target };
|
|
2488
|
-
}
|
|
2489
|
-
async function runPairCommand(command, api) {
|
|
2490
|
-
if (command.action === "list") return { pairs: await api.listPriceCrossPairs() };
|
|
2491
|
-
if (command.action === "assign") {
|
|
2492
|
-
const { pair } = await api.assignPriceCrossPair({ ceRow: command.ceRow, peRow: command.peRow, pairLots: command.pairLots });
|
|
2493
|
-
return { reason: "pair_assigned", pair };
|
|
2494
|
-
}
|
|
2495
|
-
if (command.action === "unpair") {
|
|
2496
|
-
const { pairs: _pairs, ...unpair } = await api.unpairPriceCrossPair({ pairId: command.pairId, confirmExit: true });
|
|
2497
|
-
return { reason: unpair.outcome, unpair: { pairId: command.pairId, ...unpair } };
|
|
2498
|
-
}
|
|
2499
|
-
if (command.publish) {
|
|
2500
|
-
const published = await api.publishPriceCrossReport(command.pairId);
|
|
2501
|
-
return { reason: "report_published", report: { pairId: command.pairId, published: true, url: published.url, filename: published.filename } };
|
|
2502
|
-
}
|
|
2503
|
-
const download = await api.downloadPriceCrossReport(command.pairId);
|
|
2504
|
-
const output = path3.resolve(command.output ?? path3.basename(download.filename));
|
|
2505
|
-
await writeFile(output, download.bytes, { flag: command.output ? "w" : "wx" });
|
|
2506
|
-
return { reason: "report_downloaded", report: {
|
|
2507
|
-
pairId: command.pairId,
|
|
2508
|
-
published: false,
|
|
2509
|
-
filename: download.filename,
|
|
2510
|
-
path: output,
|
|
2511
|
-
bytes: download.bytes.length,
|
|
2512
|
-
sha256: createHash("sha256").update(download.bytes).digest("hex"),
|
|
2513
|
-
decisionCount: download.decisionCount
|
|
2514
|
-
} };
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
// src/commands.ts
|
|
2518
|
-
init_gate_api();
|
|
2519
|
-
|
|
2520
|
-
// src/validation-scenario.ts
|
|
2521
|
-
init_gate_api();
|
|
2522
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2523
|
-
function makeTemporaryProfile(callerProfile) {
|
|
2524
|
-
const suffix = `${Date.now().toString(36)}-${process.pid.toString(36)}-${randomUUID2().slice(0, 8)}`;
|
|
2525
|
-
const prefix = callerProfile.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 20) || "cli";
|
|
2526
|
-
return `${prefix}-validate-${suffix}`;
|
|
2527
|
-
}
|
|
2528
|
-
function safeFailureMessage(error) {
|
|
2529
|
-
if (error instanceof Error) {
|
|
2530
|
-
return error.message.replace(/lmt_(?:operator_session|client_id)=[A-Za-z0-9._~-]+/g, "[REDACTED]").replace(/(password|token|credential|cookie)\s*[=:]\s*[^\s,;]+/gi, "$1=[REDACTED]");
|
|
2531
|
-
}
|
|
2532
|
-
return "The Operator Session validation failed.";
|
|
2533
|
-
}
|
|
2534
|
-
function gateUnavailable(error) {
|
|
2535
|
-
return error instanceof Error && /Could not reach the selected Live Market Terminal Origin/.test(error.message);
|
|
2536
|
-
}
|
|
2537
|
-
function waitForSignal2(operation, signal, deadlineMessage) {
|
|
2538
|
-
return new Promise((resolve, reject) => {
|
|
2539
|
-
if (signal.aborted) {
|
|
2540
|
-
reject(new Error(deadlineMessage));
|
|
2541
|
-
return;
|
|
2542
|
-
}
|
|
2543
|
-
const onAbort = () => reject(new Error(deadlineMessage));
|
|
2544
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
2545
|
-
operation.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
2546
|
-
});
|
|
2547
|
-
}
|
|
2548
|
-
function actionOptions(options, profile, timeoutMs, signal) {
|
|
2549
|
-
return {
|
|
2550
|
-
target: options.target,
|
|
2551
|
-
profile,
|
|
2552
|
-
store: options.store,
|
|
2553
|
-
timeoutMs,
|
|
2554
|
-
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {},
|
|
2555
|
-
signal,
|
|
2556
|
-
...options.secretReader ? { secretReader: options.secretReader } : {},
|
|
2557
|
-
...options.readLoginCredentials ? { readLoginCredentials: options.readLoginCredentials } : {}
|
|
2558
|
-
};
|
|
2559
|
-
}
|
|
2560
|
-
async function runOperatorSessionValidation(options) {
|
|
2561
|
-
const started = performance.now();
|
|
2562
|
-
const actionTimeoutMs = options.timeoutMs ?? 12e4;
|
|
2563
|
-
const actionDeadlineAt = Date.now() + actionTimeoutMs;
|
|
2564
|
-
const actionTimeoutSignal = AbortSignal.timeout(actionTimeoutMs);
|
|
2565
|
-
const actionSignal = options.signal ? AbortSignal.any([options.signal, actionTimeoutSignal]) : actionTimeoutSignal;
|
|
2566
|
-
const temporaryProfile = makeTemporaryProfile(options.callerProfile);
|
|
2567
|
-
const invalidProfile = `${temporaryProfile.slice(0, 54)}-invalid`;
|
|
2568
|
-
const observations = [];
|
|
2569
|
-
let outcome = "fail";
|
|
2570
|
-
let reason = "scenario_failed";
|
|
2571
|
-
let observedOperator2;
|
|
2572
|
-
let requestId;
|
|
2573
|
-
let buildId;
|
|
2574
|
-
let cleanup = { outcome: "success", removedProfiles: [] };
|
|
2575
|
-
try {
|
|
2576
|
-
const smsCredential = await waitForSignal2(
|
|
2577
|
-
options.store.getSmsCredential(),
|
|
2578
|
-
actionSignal,
|
|
2579
|
-
"The validation exceeded its deadline."
|
|
2580
|
-
);
|
|
2581
|
-
if (!smsCredential) {
|
|
2582
|
-
outcome = "blocked";
|
|
2583
|
-
reason = "sms_credential_missing";
|
|
2584
|
-
observations.push("Protected SMS credential is not provisioned.");
|
|
2585
|
-
} else {
|
|
2586
|
-
let credentials;
|
|
2587
|
-
try {
|
|
2588
|
-
credentials = await waitForSignal2(
|
|
2589
|
-
(options.readLoginCredentials ?? readOperatorLoginCredentials)({
|
|
2590
|
-
operatorId: options.target.operatorId,
|
|
2591
|
-
gateScope: options.target.gateScope,
|
|
2592
|
-
smsEndpoint: options.target.smsEndpoint,
|
|
2593
|
-
smsCredential,
|
|
2594
|
-
timeoutMs: Math.max(1, actionDeadlineAt - Date.now()),
|
|
2595
|
-
signal: actionSignal,
|
|
2596
|
-
...options.secretReader ? { secretReader: options.secretReader } : {}
|
|
2597
|
-
}),
|
|
2598
|
-
actionSignal,
|
|
2599
|
-
"The validation exceeded its deadline."
|
|
2600
|
-
);
|
|
2601
|
-
} catch (error) {
|
|
2602
|
-
if (actionSignal.aborted) {
|
|
2603
|
-
outcome = "fail";
|
|
2604
|
-
reason = "deadline_exceeded";
|
|
2605
|
-
observations.push("The validation exceeded its deadline while reading the Operator Directory.");
|
|
2606
|
-
} else if (error instanceof SmsReadError && ["sms_credential_missing", "sms_unavailable"].includes(error.code)) {
|
|
2607
|
-
outcome = "blocked";
|
|
2608
|
-
reason = error.code;
|
|
2609
|
-
observations.push("Operator Directory credentials could not be read from the selected Gate SMS scope.");
|
|
2610
|
-
} else {
|
|
2611
|
-
outcome = "fail";
|
|
2612
|
-
reason = error instanceof SmsReadError ? error.code : "operator_directory_invalid";
|
|
2613
|
-
observations.push("The selected Gate Operator Directory could not provide the configured Operator credentials.");
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
if (outcome === "fail" && reason === "scenario_failed" && credentials) {
|
|
2617
|
-
const validActions = createOperatorSessionActions(actionOptions(options, temporaryProfile, Math.max(1, actionDeadlineAt - Date.now()), actionSignal));
|
|
2618
|
-
try {
|
|
2619
|
-
const login = await waitForSignal2(
|
|
2620
|
-
validActions.loginWithPassword(credentials.username, credentials.password),
|
|
2621
|
-
actionSignal,
|
|
2622
|
-
"The validation exceeded its deadline."
|
|
2623
|
-
);
|
|
2624
|
-
observedOperator2 = login.observedOperator;
|
|
2625
|
-
requestId = login.metadata?.requestId;
|
|
2626
|
-
buildId = login.metadata?.buildId;
|
|
2627
|
-
observations.push("Gate accepted the configured Operator Login credentials.");
|
|
2628
|
-
const laterInvocation = createOperatorSessionActions(actionOptions(options, temporaryProfile, Math.max(1, actionDeadlineAt - Date.now()), actionSignal));
|
|
2629
|
-
const reused = await waitForSignal2(laterInvocation.statusWithBrokerProof(), actionSignal, "The validation exceeded its deadline.");
|
|
2630
|
-
observedOperator2 = reused.observedOperator;
|
|
2631
|
-
requestId = reused.metadata?.requestId ?? requestId;
|
|
2632
|
-
buildId = reused.metadata?.buildId ?? buildId;
|
|
2633
|
-
observations.push("A fresh client invocation reused the same Gate session and observed Operator.");
|
|
2634
|
-
const invalidActions = createOperatorSessionActions(actionOptions(options, invalidProfile, Math.max(1, actionDeadlineAt - Date.now()), actionSignal));
|
|
2635
|
-
let invalidLoginRejected = false;
|
|
2636
|
-
try {
|
|
2637
|
-
await waitForSignal2(
|
|
2638
|
-
invalidActions.loginWithPassword(credentials.username, `${credentials.password}.invalid`),
|
|
2639
|
-
actionSignal,
|
|
2640
|
-
"The validation exceeded its deadline."
|
|
2641
|
-
);
|
|
2642
|
-
} catch (error) {
|
|
2643
|
-
if (error instanceof OperatorCredentialsRejectedError) {
|
|
2644
|
-
invalidLoginRejected = true;
|
|
2645
|
-
observations.push("Gate rejected invalid credentials from a client without a usable session.");
|
|
2646
|
-
} else {
|
|
2647
|
-
throw error;
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
if (!invalidLoginRejected) {
|
|
2651
|
-
outcome = "fail";
|
|
2652
|
-
reason = "invalid_credentials_accepted";
|
|
2653
|
-
observations.push("Gate accepted the invalid Operator Login credentials.");
|
|
2654
|
-
} else {
|
|
2655
|
-
const invalidLaterInvocation = createOperatorSessionActions(actionOptions(options, invalidProfile, Math.max(1, actionDeadlineAt - Date.now()), actionSignal));
|
|
2656
|
-
try {
|
|
2657
|
-
await waitForSignal2(invalidLaterInvocation.statusWithBrokerProof(), actionSignal, "The validation exceeded its deadline.");
|
|
2658
|
-
outcome = "fail";
|
|
2659
|
-
reason = "invalid_login_established_session";
|
|
2660
|
-
observations.push("The unauthenticated test client unexpectedly had a usable Operator Session.");
|
|
2661
|
-
} catch (error) {
|
|
2662
|
-
if (!(error instanceof OperatorSessionRejectedError)) throw error;
|
|
2663
|
-
outcome = "pass";
|
|
2664
|
-
reason = "operator_session_validated";
|
|
2665
|
-
observations.push("Gate confirmed that the invalid-login client has no usable Operator Session.");
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
} catch (error) {
|
|
2669
|
-
if (actionSignal.aborted) {
|
|
2670
|
-
outcome = "fail";
|
|
2671
|
-
reason = "deadline_exceeded";
|
|
2672
|
-
observations.push("The Operator Session validation exceeded its deadline.");
|
|
2673
|
-
} else if (gateUnavailable(error)) {
|
|
2674
|
-
outcome = "blocked";
|
|
2675
|
-
reason = "gate_unreachable";
|
|
2676
|
-
observations.push("The selected Gate could not be reached during the validation scenario.");
|
|
2677
|
-
} else {
|
|
2678
|
-
outcome = "fail";
|
|
2679
|
-
reason = error instanceof OperatorSessionRejectedError ? "session_rejected_after_login" : "validation_action_failed";
|
|
2680
|
-
observations.push(safeFailureMessage(error));
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
2685
|
-
} catch (error) {
|
|
2686
|
-
if (actionSignal.aborted) {
|
|
2687
|
-
outcome = "fail";
|
|
2688
|
-
reason = "deadline_exceeded";
|
|
2689
|
-
observations.push("The Operator Session validation exceeded its deadline.");
|
|
2690
|
-
} else if (gateUnavailable(error)) {
|
|
2691
|
-
outcome = "blocked";
|
|
2692
|
-
reason = "gate_unreachable";
|
|
2693
|
-
observations.push("The selected Gate could not be reached.");
|
|
2694
|
-
} else {
|
|
2695
|
-
outcome = "fail";
|
|
2696
|
-
reason = "validation_action_failed";
|
|
2697
|
-
observations.push(safeFailureMessage(error));
|
|
2698
|
-
}
|
|
2699
|
-
} finally {
|
|
2700
|
-
const cleanupTimeoutMs = options.cleanupTimeoutMs ?? 1e4;
|
|
2701
|
-
const cleanupSignal = AbortSignal.timeout(cleanupTimeoutMs);
|
|
2702
|
-
const removedProfiles = [];
|
|
2703
|
-
let cleanupError;
|
|
2704
|
-
for (const profile of [invalidProfile, temporaryProfile]) {
|
|
2705
|
-
try {
|
|
2706
|
-
const saved = await waitForSignal2(
|
|
2707
|
-
options.store.getSession(profile, options.target),
|
|
2708
|
-
cleanupSignal,
|
|
2709
|
-
"Validation cleanup exceeded its deadline."
|
|
2710
|
-
);
|
|
2711
|
-
if (saved?.operatorSessionCookie) {
|
|
2712
|
-
const cleanupActions = createOperatorSessionActions(actionOptions(options, profile, cleanupTimeoutMs, cleanupSignal));
|
|
2713
|
-
try {
|
|
2714
|
-
await waitForSignal2(cleanupActions.logout(), cleanupSignal, "Validation cleanup exceeded its deadline.");
|
|
2715
|
-
} catch (error) {
|
|
2716
|
-
cleanupError ??= { code: cleanupSignal.aborted ? "cleanup_deadline_exceeded" : "gate_logout_failed", message: safeFailureMessage(error) };
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
} catch (error) {
|
|
2720
|
-
cleanupError ??= {
|
|
2721
|
-
code: cleanupSignal.aborted ? "cleanup_deadline_exceeded" : "cleanup_store_read_failed",
|
|
2722
|
-
message: safeFailureMessage(error)
|
|
2723
|
-
};
|
|
2724
|
-
}
|
|
2725
|
-
try {
|
|
2726
|
-
await waitForSignal2(
|
|
2727
|
-
options.store.removeTarget(profile, options.target),
|
|
2728
|
-
cleanupSignal,
|
|
2729
|
-
"Validation cleanup exceeded its deadline."
|
|
2730
|
-
);
|
|
2731
|
-
removedProfiles.push(profile);
|
|
2732
|
-
} catch (error) {
|
|
2733
|
-
cleanupError ??= {
|
|
2734
|
-
code: cleanupSignal.aborted ? "cleanup_deadline_exceeded" : "cleanup_store_remove_failed",
|
|
2735
|
-
message: safeFailureMessage(error)
|
|
2736
|
-
};
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
cleanup = cleanupError ? { outcome: "fail", removedProfiles, error: cleanupError } : { outcome: "success", removedProfiles };
|
|
2740
|
-
if (cleanupError && outcome === "pass") {
|
|
2741
|
-
outcome = "fail";
|
|
2742
|
-
reason = "cleanup_failed";
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
return {
|
|
2746
|
-
target: { environment: options.target.environment, origin: options.target.origin },
|
|
2747
|
-
profile: options.callerProfile,
|
|
2748
|
-
outcome,
|
|
2749
|
-
reason,
|
|
2750
|
-
expectedOperator: options.target.operatorId,
|
|
2751
|
-
...observedOperator2 ? { observedOperator: observedOperator2 } : {},
|
|
2752
|
-
temporaryProfile,
|
|
2753
|
-
observations,
|
|
2754
|
-
elapsedMs: Math.round(performance.now() - started),
|
|
2755
|
-
...requestId ? { requestId } : {},
|
|
2756
|
-
...buildId ? { buildId } : {},
|
|
2757
|
-
cleanup
|
|
2758
|
-
};
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
// src/commands.ts
|
|
2762
|
-
init_profile_name();
|
|
2763
|
-
var defaultOut = { write: (chunk) => process.stdout.write(chunk) };
|
|
2764
|
-
var defaultErr = { write: (chunk) => process.stderr.write(chunk) };
|
|
2765
|
-
var InvocationError = class extends Error {
|
|
2766
|
-
constructor(message, code = "invalid_invocation") {
|
|
2767
|
-
super(message);
|
|
2768
|
-
this.code = code;
|
|
2769
|
-
}
|
|
2770
|
-
code;
|
|
2771
|
-
};
|
|
2772
|
-
function parseTimeout(raw) {
|
|
2773
|
-
const match = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(raw.trim());
|
|
2774
|
-
if (!match) throw new InvocationError("Timeout must be a finite duration such as 30000ms, 30s, or 2m.");
|
|
2775
|
-
const value = Number(match[1]) * (match[2] === "m" ? 6e4 : match[2] === "s" ? 1e3 : 1);
|
|
2776
|
-
if (!Number.isFinite(value) || value < 1 || value > 6e5) {
|
|
2777
|
-
throw new InvocationError("Timeout must be between 1ms and 10m.");
|
|
2778
|
-
}
|
|
2779
|
-
return Math.ceil(value);
|
|
2780
|
-
}
|
|
2781
|
-
function splitHeadlessArgv(argv) {
|
|
2782
|
-
if (hasArgumentOtp(argv)) throw new InvocationError("--otp arguments are no longer accepted. Use --otp-stdin with an explicit --attempt ID.");
|
|
2783
|
-
if (argv.length === 0 || argv[0] === "interactive") {
|
|
2784
|
-
throw new InvocationError("Choose a headless command or run `live-market-terminal interactive`.");
|
|
2785
|
-
}
|
|
2786
|
-
const options = /* @__PURE__ */ new Map();
|
|
2787
|
-
let credentialStdin = false;
|
|
2788
|
-
let otpStdin = false;
|
|
2789
|
-
const positional = [];
|
|
2790
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
2791
|
-
const token = argv[index];
|
|
2792
|
-
if (token === "--otp-stdin") {
|
|
2793
|
-
if (otpStdin) throw new InvocationError("Argument --otp-stdin may be specified only once.");
|
|
2794
|
-
otpStdin = true;
|
|
2795
|
-
continue;
|
|
2796
|
-
}
|
|
2797
|
-
if (token === "--credential-stdin") {
|
|
2798
|
-
if (credentialStdin) throw new InvocationError("Argument --credential-stdin may be specified only once.");
|
|
2799
|
-
credentialStdin = true;
|
|
2800
|
-
continue;
|
|
2801
|
-
}
|
|
2802
|
-
if (token === "--env" || token === "--profile" || token === "--timeout" || token === "--attempt" || token === "--date") {
|
|
2803
|
-
const value = argv[index + 1];
|
|
2804
|
-
if (value === void 0 || value.startsWith("--")) {
|
|
2805
|
-
throw new InvocationError(`Invalid or incomplete command argument: ${token}`);
|
|
2806
|
-
}
|
|
2807
|
-
if (options.has(token)) throw new InvocationError(`Argument ${token} may be specified only once.`);
|
|
2808
|
-
options.set(token, value);
|
|
2809
|
-
index += 1;
|
|
2810
|
-
continue;
|
|
2811
|
-
}
|
|
2812
|
-
if (token.startsWith("--")) {
|
|
2813
|
-
throw new InvocationError(`Unsupported command option: ${token.split("=")[0]}`);
|
|
2814
|
-
}
|
|
2815
|
-
positional.push(token);
|
|
2816
|
-
}
|
|
2817
|
-
return { positional, options, credentialStdin, otpStdin };
|
|
2818
|
-
}
|
|
2819
|
-
function parseArguments(argv) {
|
|
2820
|
-
if (!hasArgumentOtp(argv) && isPairInvocation(argv)) return parsePairArguments(argv, parseTimeout, validateCommandProfile);
|
|
2821
|
-
const parsedInput = splitHeadlessArgv(argv.filter((value) => value !== "--stdin"));
|
|
2822
|
-
const commandVerb = parsedInput.positional[0];
|
|
2823
|
-
if (commandVerb === "settings" || commandVerb === "token") {
|
|
2824
|
-
const stdin = argv.includes("--stdin");
|
|
2825
|
-
if (argv.filter((value) => value === "--stdin").length > 1) throw new InvocationError("--stdin may be supplied only once.");
|
|
2826
|
-
const parsed = parsedInput;
|
|
2827
|
-
const [kind2, action] = parsed.positional;
|
|
2828
|
-
if (parsed.positional.length !== 2 || parsed.credentialStdin || parsed.otpStdin || [...parsed.options.keys()].some((key) => !["--env", "--profile", "--timeout"].includes(key)) || !(kind2 === "settings" && (action === "read" && !stdin || action === "save" && stdin) || kind2 === "token" && (action === "replace" && stdin || action === "read" && !stdin))) {
|
|
2829
|
-
throw new InvocationError("Use settings read, settings save --stdin, or token replace --stdin.");
|
|
2830
|
-
}
|
|
2831
|
-
const environment2 = parsed.options.get("--env"), profile2 = parsed.options.get("--profile");
|
|
2832
|
-
if (!environment2 || !profile2) throw new InvocationError("Headless commands require --env local|dev and --profile NAME.");
|
|
2833
|
-
validateCommandProfile(profile2);
|
|
2834
|
-
return { kind: kind2, action, environment: environment2, profile: profile2, ...parsed.options.has("--timeout") ? { timeoutMs: parseTimeout(parsed.options.get("--timeout")) } : {} };
|
|
2835
|
-
}
|
|
2836
|
-
if (argv.includes("--stdin")) throw new InvocationError("--stdin is only supported for Settings save and token replacement.");
|
|
2837
|
-
const { positional, options, credentialStdin, otpStdin } = parsedInput;
|
|
2838
|
-
if (positional[0] === "candle-export") {
|
|
2839
|
-
const instrument = positional[1]?.match(/^([A-Za-z][A-Za-z0-9]{0,31}):([^:\s]{1,100})$/);
|
|
2840
|
-
const date = options.get("--date");
|
|
2841
|
-
const parsedDate = date ? /* @__PURE__ */ new Date(`${date}T00:00:00Z`) : void 0;
|
|
2842
|
-
if (positional.length !== 2 || !instrument || credentialStdin || otpStdin || options.has("--attempt") || date !== void 0 && (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(parsedDate.getTime()) || parsedDate.toISOString().slice(0, 10) !== date || date < "2000-01-01")) {
|
|
2843
|
-
throw new InvocationError("Use candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD].");
|
|
2844
|
-
}
|
|
2845
|
-
const environment2 = options.get("--env");
|
|
2846
|
-
const profile2 = options.get("--profile");
|
|
2847
|
-
if (!environment2 || !profile2) throw new InvocationError("Headless commands require both --env local|dev and --profile NAME.");
|
|
2848
|
-
validateCommandProfile(profile2);
|
|
2849
|
-
return {
|
|
2850
|
-
kind: "candle-export",
|
|
2851
|
-
exchange: instrument[1].toUpperCase(),
|
|
2852
|
-
tradingSymbol: instrument[2].toUpperCase(),
|
|
2853
|
-
environment: environment2,
|
|
2854
|
-
profile: profile2,
|
|
2855
|
-
...date ? { date } : {},
|
|
2856
|
-
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2857
|
-
};
|
|
2858
|
-
}
|
|
2859
|
-
if (options.has("--date")) throw new InvocationError("--date is only supported for candle-export.");
|
|
2860
|
-
if (positional[0] === "zerodha") {
|
|
2861
|
-
const action = positional[1];
|
|
2862
|
-
if (!["login", "observe", "otp", "status"].includes(action) || positional.length !== 2 || credentialStdin) {
|
|
2863
|
-
throw new InvocationError("Use zerodha login, observe, otp, or status.");
|
|
2864
|
-
}
|
|
2865
|
-
const environment2 = options.get("--env");
|
|
2866
|
-
const profile2 = options.get("--profile");
|
|
2867
|
-
if (!environment2 || !profile2) throw new InvocationError("Headless commands require both --env local|dev and --profile NAME.");
|
|
2868
|
-
validateCommandProfile(profile2);
|
|
2869
|
-
const attemptId = options.get("--attempt");
|
|
2870
|
-
if (attemptId && (action === "login" || action === "status") || attemptId && !/^[a-zA-Z0-9_-]{1,200}$/.test(attemptId)) {
|
|
2871
|
-
throw new InvocationError("--attempt must be a valid Zerodha attempt ID and is only supported for observe and otp.");
|
|
2872
|
-
}
|
|
2873
|
-
if ((action === "observe" || action === "otp") && !attemptId) {
|
|
2874
|
-
throw new InvocationError("--attempt ID is required for zerodha observe and otp. Use the attemptId returned by zerodha login.");
|
|
2875
|
-
}
|
|
2876
|
-
if (action === "otp" && !otpStdin || otpStdin && action !== "otp") {
|
|
2877
|
-
throw new InvocationError("--otp-stdin is required for zerodha otp only. Start with zerodha login, then submit with zerodha otp --attempt ID --otp-stdin.");
|
|
2878
|
-
}
|
|
2879
|
-
return {
|
|
2880
|
-
kind: "zerodha",
|
|
2881
|
-
action,
|
|
2882
|
-
environment: environment2,
|
|
2883
|
-
profile: profile2,
|
|
2884
|
-
attemptId,
|
|
2885
|
-
otpStdin,
|
|
2886
|
-
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2887
|
-
};
|
|
2888
|
-
}
|
|
2889
|
-
if (positional[0] === "validate" && positional[1] === "zerodha-session") {
|
|
2890
|
-
if (positional.length !== 2 || credentialStdin) throw new InvocationError("Use validate zerodha-session with optional --attempt ID and --otp-stdin.");
|
|
2891
|
-
const environment2 = options.get("--env");
|
|
2892
|
-
const profile2 = options.get("--profile");
|
|
2893
|
-
if (!environment2 || !profile2) throw new InvocationError("Headless commands require both --env local|dev and --profile NAME.");
|
|
2894
|
-
validateCommandProfile(profile2);
|
|
2895
|
-
const attemptId = options.get("--attempt");
|
|
2896
|
-
if (attemptId !== void 0 && !/^[a-zA-Z0-9_-]{1,200}$/.test(attemptId)) throw new InvocationError("--attempt must be a valid Zerodha attempt ID.");
|
|
2897
|
-
if (otpStdin && !attemptId) throw new InvocationError("--otp-stdin requires an explicit --attempt ID from a prior login or validation.");
|
|
2898
|
-
return {
|
|
2899
|
-
kind: "validate-zerodha",
|
|
2900
|
-
environment: environment2,
|
|
2901
|
-
profile: profile2,
|
|
2902
|
-
attemptId,
|
|
2903
|
-
otpStdin,
|
|
2904
|
-
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2905
|
-
};
|
|
2906
|
-
}
|
|
2907
|
-
if (otpStdin || options.has("--attempt")) throw new InvocationError("--otp-stdin and --attempt are Zerodha options only.");
|
|
2908
|
-
if (positional[0] === "credential") {
|
|
2909
|
-
if (positional[1] !== "provision" || positional.length > 2) {
|
|
2910
|
-
throw new InvocationError("Use `credential provision` to provision the protected SMS credential.");
|
|
2911
|
-
}
|
|
2912
|
-
const environment2 = options.get("--env");
|
|
2913
|
-
const profile2 = options.get("--profile");
|
|
2914
|
-
if (!credentialStdin || !environment2 || !profile2) {
|
|
2915
|
-
throw new InvocationError("Headless credential provision requires --env local|dev, --profile NAME, and --credential-stdin.");
|
|
2916
|
-
}
|
|
2917
|
-
validateCommandProfile(profile2);
|
|
2918
|
-
return { kind: "credential-provision", environment: environment2, profile: profile2 };
|
|
2919
|
-
}
|
|
2920
|
-
if (credentialStdin) throw new InvocationError("--credential-stdin is only supported for credential provision.");
|
|
2921
|
-
let kind;
|
|
2922
|
-
if (positional[0] === "login" || positional[0] === "status" || positional[0] === "logout") {
|
|
2923
|
-
kind = positional[0];
|
|
2924
|
-
if (positional.length > 1) {
|
|
2925
|
-
throw new InvocationError("Unexpected positional argument; use --help for command syntax.");
|
|
2926
|
-
}
|
|
2927
|
-
} else if (positional[0] === "validate" && positional[1] === "operator-session") {
|
|
2928
|
-
kind = "validate";
|
|
2929
|
-
if (positional.length > 2) {
|
|
2930
|
-
throw new InvocationError("Unexpected positional argument; use --help for command syntax.");
|
|
2931
|
-
}
|
|
2932
|
-
} else if (positional.length === 0) {
|
|
2933
|
-
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session|zerodha-session.");
|
|
2934
|
-
} else {
|
|
2935
|
-
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session|zerodha-session.");
|
|
2936
|
-
}
|
|
2937
|
-
const environment = options.get("--env");
|
|
2938
|
-
const profile = options.get("--profile");
|
|
2939
|
-
if (!environment || !profile) throw new InvocationError("Headless commands require both --env local|dev and --profile NAME.");
|
|
2940
|
-
validateCommandProfile(profile);
|
|
2941
|
-
const timeoutMs = options.has("--timeout") ? parseTimeout(options.get("--timeout")) : void 0;
|
|
2942
|
-
return { kind, environment, profile, ...timeoutMs === void 0 ? {} : { timeoutMs } };
|
|
2943
|
-
}
|
|
2944
|
-
function validateCommandProfile(profile) {
|
|
2945
|
-
try {
|
|
2946
|
-
assertClientProfileName(profile);
|
|
2947
|
-
} catch {
|
|
2948
|
-
throw new InvocationError(CLIENT_PROFILE_NAME_ERROR);
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
function durationFor(command) {
|
|
2952
|
-
return command.kind === "validate" || command.kind === "validate-zerodha" ? command.timeoutMs ?? 12e4 : command.kind === "credential-provision" ? 3e4 : command.timeoutMs ?? 3e4;
|
|
2953
|
-
}
|
|
2954
|
-
function errorDetails(error, signal) {
|
|
2955
|
-
if (signal?.aborted) return { code: "deadline_exceeded", message: "The action exceeded its deadline.", exitCode: 1 };
|
|
2956
|
-
if (error instanceof InvocationError || error instanceof ProtectedInputError || error instanceof PairInvocationError) {
|
|
2957
|
-
return { code: error.code, message: error.message, exitCode: 2 };
|
|
2958
|
-
}
|
|
2959
|
-
if (error instanceof OperatorSessionRejectedError) {
|
|
2960
|
-
return { code: "operator_login_required", message: "No valid Operator Session is available; run login first.", exitCode: 3 };
|
|
2961
|
-
}
|
|
2962
|
-
if (error instanceof OperatorCredentialsRejectedError) {
|
|
2963
|
-
return { code: "incorrect_credentials", message: "Gate rejected the Operator Login credentials.", exitCode: 1 };
|
|
2964
|
-
}
|
|
2965
|
-
if (error instanceof ZerodhaLoginAttemptUnavailableError) return { code: "login_attempt_inactive", message: error.message, exitCode: 1 };
|
|
2966
|
-
if (error instanceof ZerodhaLoginFailedError) return { code: "zerodha_login_failed", message: error.message, exitCode: 1 };
|
|
2967
|
-
if (error instanceof Error && "code" in error && typeof error.code === "string" && ["zerodha_session_absent", "zerodha_session_restoring", "zerodha_verification_unavailable", "zerodha_credentials_missing", "secret_store_unavailable", "operator_not_configured"].includes(error.code)) {
|
|
2968
|
-
return { code: error.code, message: error.message, exitCode: 1 };
|
|
2969
|
-
}
|
|
2970
|
-
if (error instanceof SmsReadError) {
|
|
2971
|
-
return { code: error.code, message: error.message, exitCode: 1 };
|
|
2972
|
-
}
|
|
2973
|
-
if (error instanceof Error && error.message === "The CLI supports local or dev targets only; prod is not permitted.") {
|
|
2974
|
-
return { code: "invalid_target", message: error.message, exitCode: 2 };
|
|
2975
|
-
}
|
|
2976
|
-
return {
|
|
2977
|
-
code: "action_failed",
|
|
2978
|
-
message: error instanceof Error ? error.message : "The CLI action failed.",
|
|
2979
|
-
exitCode: 1
|
|
2980
|
-
};
|
|
2981
|
-
}
|
|
2982
|
-
function emit(envelope, streams, diagnostic) {
|
|
2983
|
-
streams.stdout.write(`${JSON.stringify(envelope)}
|
|
2984
|
-
`);
|
|
2985
|
-
if (diagnostic) streams.stderr.write(`${diagnostic}
|
|
2986
|
-
`);
|
|
2987
|
-
}
|
|
2988
|
-
async function readProtectedInput(stdin, signal, label = "SMS credential") {
|
|
2989
|
-
if (!stdin || stdin.isTTY) throw new InvocationError(`${label} requires piped protected stdin.`);
|
|
2990
|
-
const chunks = [];
|
|
2991
|
-
let size = 0;
|
|
2992
|
-
const iterator = stdin[Symbol.asyncIterator]();
|
|
2993
|
-
while (true) {
|
|
2994
|
-
if (signal.aborted) throw new Error("The action exceeded its deadline.");
|
|
2995
|
-
const next = await new Promise((resolve, reject) => {
|
|
2996
|
-
const onAbort = () => {
|
|
2997
|
-
stdin.destroy?.();
|
|
2998
|
-
reject(new Error("The action exceeded its deadline."));
|
|
2999
|
-
};
|
|
3000
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
3001
|
-
iterator.next().then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
3002
|
-
});
|
|
3003
|
-
if (next.done) break;
|
|
3004
|
-
const chunk = next.value;
|
|
3005
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3006
|
-
size += buffer.length;
|
|
3007
|
-
if (size > 4096) throw new InvocationError(`${label} input is too large.`);
|
|
3008
|
-
chunks.push(buffer);
|
|
3009
|
-
}
|
|
3010
|
-
const credential = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
3011
|
-
if (!credential.trim() || /[\r\n]/.test(credential)) throw new InvocationError(`${label} input must be one non-empty line.`);
|
|
3012
|
-
return credential;
|
|
3013
|
-
}
|
|
3014
|
-
async function runHeadlessCli(argv, dependencies = {}) {
|
|
3015
|
-
let otp;
|
|
3016
|
-
let securePayload;
|
|
3017
|
-
const protect = (stream) => ({ write(chunk) {
|
|
3018
|
-
let output = otp ? chunk.replaceAll(otp, "[redacted]") : chunk;
|
|
3019
|
-
if (securePayload) output = output.replaceAll(securePayload, "[redacted]");
|
|
3020
|
-
return stream.write(output);
|
|
3021
|
-
} });
|
|
3022
|
-
const streams = { stdout: protect(dependencies.stdout ?? defaultOut), stderr: protect(dependencies.stderr ?? defaultErr) };
|
|
3023
|
-
const started = performance.now();
|
|
3024
|
-
let command;
|
|
3025
|
-
let target;
|
|
3026
|
-
let profile;
|
|
3027
|
-
let actionApi;
|
|
3028
|
-
let signal;
|
|
3029
|
-
let deadlineTimer;
|
|
3030
|
-
let store;
|
|
3031
|
-
try {
|
|
3032
|
-
command = parseArguments(argv);
|
|
3033
|
-
const timeoutMs = durationFor(command);
|
|
3034
|
-
const deadline = new AbortController();
|
|
3035
|
-
deadlineTimer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
3036
|
-
signal = deadline.signal;
|
|
3037
|
-
profile = command.profile;
|
|
3038
|
-
try {
|
|
3039
|
-
target = resolveCliTarget({ LMT_ENV: command.environment }, dependencies.configPath);
|
|
3040
|
-
} catch (error) {
|
|
3041
|
-
throw new InvocationError(error instanceof Error ? error.message : "The selected target is invalid.", "invalid_target");
|
|
3042
|
-
}
|
|
3043
|
-
store = new CliProfileStore(dependencies.homeDirectory);
|
|
3044
|
-
if (command.kind === "credential-provision") {
|
|
3045
|
-
const credential = await readProtectedInput(dependencies.stdin ?? process.stdin, signal);
|
|
3046
|
-
await store.saveSmsCredential(credential);
|
|
3047
|
-
emit({
|
|
3048
|
-
version: 1,
|
|
3049
|
-
command: "credential provision",
|
|
3050
|
-
outcome: "success",
|
|
3051
|
-
reason: "sms_credential_provisioned",
|
|
3052
|
-
target: { environment: target.environment, origin: target.origin },
|
|
3053
|
-
profile,
|
|
3054
|
-
elapsedMs: Math.round(performance.now() - started),
|
|
3055
|
-
metadata: { requestId: null, buildId: null }
|
|
3056
|
-
}, streams);
|
|
3057
|
-
return 0;
|
|
3058
|
-
}
|
|
3059
|
-
if (command.kind === "validate-zerodha") {
|
|
3060
|
-
const validation = await runZerodhaValidation({
|
|
3061
|
-
target,
|
|
3062
|
-
callerProfile: profile,
|
|
3063
|
-
store,
|
|
3064
|
-
timeoutMs,
|
|
3065
|
-
signal,
|
|
3066
|
-
attemptId: command.attemptId,
|
|
3067
|
-
...command.otpStdin ? { readOtp: (inputSignal) => readProtectedOtp(dependencies.stdin ?? process.stdin, inputSignal) } : {},
|
|
3068
|
-
...dependencies.fetchImpl ? { fetchImpl: dependencies.fetchImpl } : {}
|
|
3069
|
-
});
|
|
3070
|
-
emit({
|
|
3071
|
-
version: 1,
|
|
3072
|
-
command: "validate zerodha-session",
|
|
3073
|
-
outcome: validation.outcome,
|
|
3074
|
-
reason: validation.reason,
|
|
3075
|
-
target: validation.target,
|
|
3076
|
-
profile,
|
|
3077
|
-
observedOperator: validation.observedOperator,
|
|
3078
|
-
attemptId: validation.attemptId,
|
|
3079
|
-
observedState: validation.observedState,
|
|
3080
|
-
elapsedMs: validation.elapsedMs,
|
|
3081
|
-
metadata: { requestId: validation.requestId, buildId: validation.buildId },
|
|
3082
|
-
validation
|
|
3083
|
-
}, streams, validation.exitCode === 0 ? void 0 : `Zerodha Session validation ${validation.outcome}: ${validation.reason}. ${validation.nextStep ?? ""}`);
|
|
3084
|
-
return validation.exitCode;
|
|
3085
|
-
}
|
|
3086
|
-
if (command.kind === "validate") {
|
|
3087
|
-
const validation = await runOperatorSessionValidation({
|
|
3088
|
-
target,
|
|
3089
|
-
callerProfile: profile,
|
|
3090
|
-
store,
|
|
3091
|
-
timeoutMs,
|
|
3092
|
-
signal,
|
|
3093
|
-
...dependencies.fetchImpl ? { fetchImpl: dependencies.fetchImpl } : {},
|
|
3094
|
-
...dependencies.secretReader ? { secretReader: dependencies.secretReader } : {},
|
|
3095
|
-
...dependencies.readLoginCredentials ? { readLoginCredentials: dependencies.readLoginCredentials } : {}
|
|
3096
|
-
});
|
|
3097
|
-
const exitCode = validation.outcome === "pass" ? 0 : validation.outcome === "blocked" ? 4 : 1;
|
|
3098
|
-
emit({
|
|
3099
|
-
version: 1,
|
|
3100
|
-
command: "validate operator-session",
|
|
3101
|
-
outcome: validation.outcome,
|
|
3102
|
-
reason: validation.reason,
|
|
3103
|
-
target: { environment: target.environment, origin: target.origin },
|
|
3104
|
-
profile,
|
|
3105
|
-
...validation.observedOperator ? { observedOperator: validation.observedOperator } : {},
|
|
3106
|
-
elapsedMs: validation.elapsedMs,
|
|
3107
|
-
metadata: { requestId: validation.requestId ?? null, buildId: validation.buildId ?? null },
|
|
3108
|
-
validation
|
|
3109
|
-
}, streams, exitCode === 0 ? void 0 : `Operator Session validation ${validation.outcome}: ${validation.reason}.`);
|
|
3110
|
-
return exitCode;
|
|
3111
|
-
}
|
|
3112
|
-
const actions = createOperatorSessionActions({
|
|
3113
|
-
target,
|
|
3114
|
-
profile,
|
|
3115
|
-
store,
|
|
3116
|
-
timeoutMs,
|
|
3117
|
-
signal,
|
|
3118
|
-
...dependencies.fetchImpl ? { fetchImpl: dependencies.fetchImpl } : {},
|
|
3119
|
-
...dependencies.secretReader ? { secretReader: dependencies.secretReader } : {},
|
|
3120
|
-
...dependencies.readLoginCredentials ? { readLoginCredentials: dependencies.readLoginCredentials } : {}
|
|
3121
|
-
});
|
|
3122
|
-
actionApi = await actions.getApi();
|
|
3123
|
-
if (command.kind === "settings" || command.kind === "token") {
|
|
3124
|
-
await actions.status();
|
|
3125
|
-
let payload;
|
|
3126
|
-
if (command.action !== "read") {
|
|
3127
|
-
securePayload = await readProtectedInput(dependencies.stdin ?? process.stdin, signal, "Settings JSON");
|
|
3128
|
-
try {
|
|
3129
|
-
payload = JSON.parse(securePayload);
|
|
3130
|
-
} catch {
|
|
3131
|
-
throw new InvocationError("Protected stdin must contain one JSON object.");
|
|
3132
|
-
}
|
|
3133
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new InvocationError("Protected stdin must contain one JSON object.");
|
|
3134
|
-
}
|
|
3135
|
-
const settings = command.kind === "settings" ? command.action === "read" ? { outcome: "read", settings: await actionApi.getSettings() } : await actionApi.saveSettings(payload) : void 0;
|
|
3136
|
-
const tokenMetadata = command.kind === "token" && command.action === "read" ? await actionApi.getTokenMetadata() : void 0;
|
|
3137
|
-
const token = command.kind === "token" && command.action === "replace" ? await actionApi.replaceToken(payload) : void 0;
|
|
3138
|
-
emit({
|
|
3139
|
-
version: 1,
|
|
3140
|
-
command: command.kind + " " + command.action,
|
|
3141
|
-
outcome: "success",
|
|
3142
|
-
reason: settings?.outcome ?? token?.outcome,
|
|
3143
|
-
...settings ? { settings: settings.settings } : {},
|
|
3144
|
-
...token ? { token } : {},
|
|
3145
|
-
...tokenMetadata ? { tokenMetadata } : {},
|
|
3146
|
-
target: { environment: target.environment, origin: target.origin },
|
|
3147
|
-
profile,
|
|
3148
|
-
elapsedMs: Math.round(performance.now() - started),
|
|
3149
|
-
metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
|
|
3150
|
-
}, streams);
|
|
3151
|
-
return 0;
|
|
3152
|
-
}
|
|
3153
|
-
if (command.kind === "candle-export") {
|
|
3154
|
-
await actions.status();
|
|
3155
|
-
const candleExport = await actionApi.exportCandles({
|
|
3156
|
-
exchange: command.exchange,
|
|
3157
|
-
tradingSymbol: command.tradingSymbol,
|
|
3158
|
-
...command.date ? { date: command.date } : {}
|
|
3159
|
-
});
|
|
3160
|
-
emit({
|
|
3161
|
-
version: 1,
|
|
3162
|
-
command: command.kind,
|
|
3163
|
-
outcome: "success",
|
|
3164
|
-
target: { environment: target.environment, origin: target.origin },
|
|
3165
|
-
profile,
|
|
3166
|
-
observedOperator: target.operatorId,
|
|
3167
|
-
candleExport,
|
|
3168
|
-
elapsedMs: Math.round(performance.now() - started),
|
|
3169
|
-
metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
|
|
3170
|
-
}, streams);
|
|
3171
|
-
return 0;
|
|
3172
|
-
}
|
|
3173
|
-
if (command.kind === "pair") {
|
|
3174
|
-
await actions.status();
|
|
3175
|
-
const result2 = await runPairCommand(command, actionApi);
|
|
3176
|
-
emit({
|
|
3177
|
-
version: 1,
|
|
3178
|
-
command: `pair ${command.action}`,
|
|
3179
|
-
outcome: "success",
|
|
3180
|
-
...result2,
|
|
3181
|
-
target: { environment: target.environment, origin: target.origin },
|
|
3182
|
-
profile,
|
|
3183
|
-
observedOperator: target.operatorId,
|
|
3184
|
-
elapsedMs: Math.round(performance.now() - started),
|
|
3185
|
-
metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
|
|
3186
|
-
}, streams);
|
|
3187
|
-
return 0;
|
|
3188
|
-
}
|
|
3189
|
-
if (command.kind === "zerodha") {
|
|
3190
|
-
await actions.status();
|
|
3191
|
-
if (command.otpStdin) otp = await readProtectedOtp(dependencies.stdin ?? process.stdin, signal);
|
|
3192
|
-
const zerodha = await runZerodhaAction(
|
|
3193
|
-
actionApi,
|
|
3194
|
-
command.action,
|
|
3195
|
-
target.operatorId,
|
|
3196
|
-
signal,
|
|
3197
|
-
command.attemptId,
|
|
3198
|
-
otp
|
|
3199
|
-
);
|
|
3200
|
-
const awaitingOtp = zerodha.status === "awaiting_otp";
|
|
3201
|
-
emit({
|
|
3202
|
-
version: 1,
|
|
3203
|
-
command: `zerodha ${command.action}`,
|
|
3204
|
-
outcome: awaitingOtp ? "blocked" : "success",
|
|
3205
|
-
reason: awaitingOtp ? "awaiting_otp" : "zerodha_session_present",
|
|
3206
|
-
target: { environment: target.environment, origin: target.origin },
|
|
3207
|
-
profile,
|
|
3208
|
-
observedOperator: target.operatorId,
|
|
3209
|
-
elapsedMs: Math.round(performance.now() - started),
|
|
3210
|
-
metadata: {
|
|
3211
|
-
requestId: actionApi.responseMetadata().requestId ?? null,
|
|
3212
|
-
buildId: actionApi.responseMetadata().buildId ?? null
|
|
3213
|
-
},
|
|
3214
|
-
zerodha
|
|
3215
|
-
}, streams);
|
|
3216
|
-
return awaitingOtp ? 4 : 0;
|
|
3217
|
-
}
|
|
3218
|
-
if (command.kind === "logout") {
|
|
3219
|
-
await actions.logout();
|
|
3220
|
-
emit({
|
|
3221
|
-
version: 1,
|
|
3222
|
-
command: "logout",
|
|
3223
|
-
outcome: "success",
|
|
3224
|
-
reason: "operator_session_cleared",
|
|
3225
|
-
target: { environment: target.environment, origin: target.origin },
|
|
3226
|
-
profile,
|
|
3227
|
-
elapsedMs: Math.round(performance.now() - started),
|
|
3228
|
-
metadata: {
|
|
3229
|
-
requestId: actionApi.responseMetadata().requestId ?? null,
|
|
3230
|
-
buildId: actionApi.responseMetadata().buildId ?? null
|
|
3231
|
-
}
|
|
3232
|
-
}, streams);
|
|
3233
|
-
return 0;
|
|
3234
|
-
}
|
|
3235
|
-
const result = command.kind === "login" ? await actions.loginFromSms() : await actions.status();
|
|
3236
|
-
emit({
|
|
3237
|
-
version: 1,
|
|
3238
|
-
command: command.kind,
|
|
3239
|
-
outcome: "success",
|
|
3240
|
-
reason: command.kind === "login" ? "operator_session_established" : "operator_session_valid",
|
|
3241
|
-
target: { environment: target.environment, origin: target.origin },
|
|
3242
|
-
profile,
|
|
3243
|
-
observedOperator: result.observedOperator,
|
|
3244
|
-
elapsedMs: Math.round(performance.now() - started),
|
|
3245
|
-
metadata: { requestId: result.metadata?.requestId ?? null, buildId: result.metadata?.buildId ?? null }
|
|
3246
|
-
}, streams);
|
|
3247
|
-
return 0;
|
|
3248
|
-
} catch (error) {
|
|
3249
|
-
if (error instanceof OperatorSessionRejectedError && store && actionApi && target && profile && !signal?.aborted) {
|
|
3250
|
-
await store.saveSession(profile, target, actionApi.getCookies()).catch(() => void 0);
|
|
3251
|
-
}
|
|
3252
|
-
const details = error instanceof SettingsActionError || error instanceof PriceCrossActionError ? { code: error.code, message: error.message, exitCode: 1 } : errorDetails(error, signal);
|
|
3253
|
-
const recovery = error instanceof Error ? error : void 0;
|
|
3254
|
-
const attemptId = recovery?.attemptId ?? (command?.kind === "zerodha" ? command.attemptId : void 0);
|
|
3255
|
-
emit({
|
|
3256
|
-
version: 1,
|
|
3257
|
-
command: command?.kind === "zerodha" || command?.kind === "settings" || command?.kind === "token" || command?.kind === "pair" ? `${command.kind} ${command.action}` : command?.kind === "credential-provision" ? "credential provision" : command?.kind === "validate-zerodha" ? "validate zerodha-session" : command?.kind === "validate" ? "validate operator-session" : command?.kind ?? "",
|
|
3258
|
-
outcome: (error instanceof SettingsActionError || error instanceof PriceCrossActionError) && error.uncertain ? "uncertain" : details.exitCode === 4 ? "blocked" : "fail",
|
|
3259
|
-
reason: details.code,
|
|
3260
|
-
...target ? { target: { environment: target.environment, origin: target.origin } } : command ? { target: { environment: command.environment } } : {},
|
|
3261
|
-
...profile ? { profile } : {},
|
|
3262
|
-
...attemptId ? { attemptId, recovery: recovery?.recovery ?? "Observe this exact attempt with the same target and profile; start explicitly only if Gate reports it inactive." } : {},
|
|
3263
|
-
...!attemptId && error instanceof PriceCrossActionError && error.recovery ? { recovery: error.recovery } : {},
|
|
3264
|
-
...recovery?.observedState ? { observedState: recovery.observedState } : {},
|
|
3265
|
-
...recovery?.observedOperator ? { observedOperator: recovery.observedOperator } : {},
|
|
3266
|
-
elapsedMs: Math.round(performance.now() - started),
|
|
3267
|
-
metadata: {
|
|
3268
|
-
requestId: actionApi?.responseMetadata().requestId ?? null,
|
|
3269
|
-
buildId: actionApi?.responseMetadata().buildId ?? null
|
|
3270
|
-
},
|
|
3271
|
-
error: { code: details.code, message: details.message }
|
|
3272
|
-
}, streams, details.exitCode === 2 ? `Invalid CLI invocation: ${details.message}` : details.message);
|
|
3273
|
-
return details.exitCode;
|
|
3274
|
-
} finally {
|
|
3275
|
-
otp = void 0;
|
|
3276
|
-
securePayload = void 0;
|
|
3277
|
-
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
|
-
function hasArgumentOtp(argv) {
|
|
3281
|
-
return argv.some((token) => token === "--otp" || token.startsWith("--otp="));
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
// src/index.ts
|
|
3285
|
-
init_profile_name();
|
|
3286
|
-
|
|
3287
|
-
// src/cli-env.ts
|
|
3288
|
-
function ensureFetchIgnoresEnvProxy() {
|
|
3289
|
-
process.env.NODE_USE_ENV_PROXY = "0";
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
// src/help.ts
|
|
3293
|
-
function wantsCliHelp(argv) {
|
|
3294
|
-
return argv.includes("--help") || argv.includes("-h") || argv[0] === "help";
|
|
3295
|
-
}
|
|
3296
|
-
function formatCliHelp() {
|
|
3297
|
-
return `Live Market Terminal CLI
|
|
3298
|
-
|
|
3299
|
-
Usage (lmt is an alias of live-market-terminal):
|
|
3300
|
-
live-market-terminal interactive [--env local|dev] [--profile NAME]
|
|
3301
|
-
live-market-terminal interactive credential provision [--env local|dev] [--profile NAME]
|
|
3302
|
-
live-market-terminal login --env local|dev --profile NAME [--timeout DURATION]
|
|
3303
|
-
live-market-terminal logout --env local|dev --profile NAME [--timeout DURATION]
|
|
3304
|
-
live-market-terminal candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD] --env local|dev --profile NAME [--timeout DURATION]
|
|
3305
|
-
live-market-terminal pair list --env local|dev --profile NAME [--timeout DURATION]
|
|
3306
|
-
live-market-terminal pair assign --ce ROW --pe ROW --lots N --env local|dev --profile NAME [--timeout DURATION]
|
|
3307
|
-
live-market-terminal pair unpair PAIR_ID --confirm --env local|dev --profile NAME [--timeout DURATION]
|
|
3308
|
-
live-market-terminal pair report PAIR_ID [--publish | --output PATH] --env local|dev --profile NAME [--timeout DURATION]
|
|
3309
|
-
live-market-terminal settings read --env local|dev --profile NAME
|
|
3310
|
-
live-market-terminal settings save --stdin --env local|dev --profile NAME
|
|
3311
|
-
live-market-terminal token read --env local|dev --profile NAME
|
|
3312
|
-
live-market-terminal token replace --stdin --env local|dev --profile NAME
|
|
3313
|
-
live-market-terminal status --env local|dev --profile NAME [--timeout DURATION]
|
|
3314
|
-
live-market-terminal zerodha login --env local|dev --profile NAME [--timeout DURATION]
|
|
3315
|
-
live-market-terminal zerodha observe --attempt ID --env local|dev --profile NAME [--timeout DURATION]
|
|
3316
|
-
live-market-terminal zerodha otp --attempt ID --otp-stdin --env local|dev --profile NAME [--timeout DURATION]
|
|
3317
|
-
live-market-terminal zerodha status --env local|dev --profile NAME [--timeout DURATION]
|
|
3318
|
-
live-market-terminal validate operator-session --env local|dev --profile NAME [--timeout DURATION]
|
|
3319
|
-
live-market-terminal validate zerodha-session --env local|dev --profile NAME [--attempt ID [--otp-stdin]] [--timeout DURATION]
|
|
3320
|
-
live-market-terminal credential provision --env local|dev --profile NAME --credential-stdin
|
|
3321
|
-
|
|
3322
|
-
Headless options (--env, --profile, --timeout, --attempt, --otp-stdin, --credential-stdin) may appear before or after the command.
|
|
3323
|
-
|
|
3324
|
-
From the monorepo root:
|
|
3325
|
-
pnpm cli [--profile NAME] # interactive; --env defaults to local
|
|
3326
|
-
pnpm cli --env dev --profile my-dev-session
|
|
3327
|
-
pnpm -s cli:headless status --env local --profile default # build step runs quietly (-s)
|
|
3328
|
-
|
|
3329
|
-
Candle Export publishes a new immutable workbook link and returns its context in candleExport JSON.
|
|
3330
|
-
Empty or unavailable sources are explained in the workbook. Export never starts Zerodha login or token recovery.
|
|
3331
|
-
|
|
3332
|
-
Price Cross pair commands operate Paper pairs on the owning Broker through Gate; they never save Settings or place Orders.
|
|
3333
|
-
pair list returns current and past pairs (JSON pairs.active / pairs.past). Broker refusals keep their reason codes
|
|
3334
|
-
(invalid_lots, same_type, already_paired, order_in_flight, flag_disabled, ...) with exit 1.
|
|
3335
|
-
pair unpair requires --confirm and returns exit details (unpair.exit) when a paper position closed; a repeat reports already_unpaired.
|
|
3336
|
-
pair report downloads the lifetime workbook (default: server filename in the current directory, never overwritten);
|
|
3337
|
-
--publish returns a new immutable Hosted URL instead.
|
|
3338
|
-
A pair assign, unpair or publish timeout after submission is uncertain (exit 1); read pair list before resubmitting.
|
|
3339
|
-
The CLI never repeats a submission.
|
|
3340
|
-
|
|
3341
|
-
Settings save reads one JSON object from stdin: expectedSettingsVersion plus selected rowCount,
|
|
3342
|
-
marketProtection, entryCategories, or complete credentials. Omitted fields are preserved.
|
|
3343
|
-
Token replace reads {accessToken?, expected, continueReplacement?, verifyCurrent?} from protected stdin.
|
|
3344
|
-
Use verifyCurrent: true with expected from token read to validate the saved token without supplying accessToken.
|
|
3345
|
-
Read token metadata through token read. A timeout after submission is uncertain; never retry blindly.
|
|
3346
|
-
|
|
3347
|
-
Targets: local and dev only. Production is not permitted.
|
|
3348
|
-
|
|
3349
|
-
Zerodha login is two steps: zerodha login, then zerodha otp --attempt ID --otp-stdin.
|
|
3350
|
-
Obtain the OTP from Kite after login returns awaiting_otp; expired attempts require a new login.
|
|
3351
|
-
OTP arguments (--otp CODE and --otp=CODE) are rejected. Pipe protected input; headless mode never prompts.
|
|
3352
|
-
|
|
3353
|
-
Validate Zerodha Session starts a new attempt; resume the returned ID with --attempt ID.
|
|
3354
|
-
Supply --otp-stdin only when submitting a human OTP to that exact attempt.
|
|
3355
|
-
Validation retains the caller profile session and can replace Broker's active attempt or refresh its shared token.
|
|
3356
|
-
Interactive mode offers Validate Zerodha Session and explicit Resume Zerodha Validation.
|
|
3357
|
-
Interactive mode restores a saved Operator Session when Gate still accepts it.
|
|
3358
|
-
Headless commands write one JSON object per line to stdout; diagnostics go to stderr.
|
|
3359
|
-
|
|
3360
|
-
Exit codes (headless):
|
|
3361
|
-
0 success or validation pass
|
|
3362
|
-
1 action failure
|
|
3363
|
-
2 invalid invocation or target
|
|
3364
|
-
3 Operator Login required
|
|
3365
|
-
4 validation blocked or Zerodha awaiting OTP
|
|
3366
|
-
|
|
3367
|
-
Options:
|
|
3368
|
-
--env local|dev Target environment (interactive default: local; headless: required)
|
|
3369
|
-
--profile NAME Client profile (default: default for interactive)
|
|
3370
|
-
--timeout DURATION Action deadline, e.g. 45s, 2m, 30000ms (headless)
|
|
3371
|
-
--ce ROW / --pe ROW Market Watch rows for pair assign (CE leg, PE leg)
|
|
3372
|
-
--lots N Pair Lots for pair assign; the Broker enforces 1 through its configured maximum
|
|
3373
|
-
--confirm Required by pair unpair
|
|
3374
|
-
--publish pair report: publish an immutable snapshot and return its URL
|
|
3375
|
-
--output PATH pair report: write the downloaded workbook to PATH (overwrites)
|
|
3376
|
-
--date YYYY-MM-DD Single IST date for candle-export; omitted: latest completed exchange session
|
|
3377
|
-
--attempt ID Attempt ID returned by zerodha login (required for observe and otp)
|
|
3378
|
-
--otp-stdin Read one six-digit OTP line from piped protected stdin (maximum 8 bytes)
|
|
3379
|
-
--credential-stdin Read SMS credential from piped stdin (headless provision)
|
|
3380
|
-
--help, -h Show this help
|
|
3381
|
-
|
|
3382
|
-
Storage: ~/.lmt/secrets.pem (SMS credential and per-profile Gate cookies)
|
|
3383
|
-
`;
|
|
3384
|
-
}
|
|
3385
|
-
|
|
3386
|
-
// src/index.ts
|
|
3387
|
-
var CliInvocationError = class extends Error {
|
|
3388
|
-
};
|
|
3389
|
-
function normalizeArgv(argv) {
|
|
3390
|
-
return argv[0] === "--" ? argv.slice(1) : argv;
|
|
3391
|
-
}
|
|
3392
|
-
async function bootstrapCli(dependencies = {}) {
|
|
3393
|
-
if (dependencies.runApplication) {
|
|
3394
|
-
await dependencies.runApplication();
|
|
3395
|
-
return;
|
|
3396
|
-
}
|
|
3397
|
-
const argv = normalizeArgv(process.argv.slice(2));
|
|
3398
|
-
if (hasArgumentOtp(argv)) {
|
|
3399
|
-
process.exitCode = await runHeadlessCli(argv);
|
|
3400
|
-
return;
|
|
3401
|
-
}
|
|
3402
|
-
if (wantsCliHelp(argv)) {
|
|
3403
|
-
process.stdout.write(formatCliHelp());
|
|
3404
|
-
return;
|
|
3405
|
-
}
|
|
3406
|
-
if (argv[0] !== "interactive") {
|
|
3407
|
-
process.exitCode = await runHeadlessCli(argv);
|
|
3408
|
-
return;
|
|
3409
|
-
}
|
|
3410
|
-
let parsed;
|
|
3411
|
-
let target;
|
|
3412
|
-
try {
|
|
3413
|
-
parsed = parseInteractiveArguments(argv);
|
|
3414
|
-
target = resolveCliTarget({ LMT_ENV: parsed.environment });
|
|
3415
|
-
} catch (error) {
|
|
3416
|
-
throw new CliInvocationError(error instanceof Error ? error.message : "Invalid interactive CLI configuration.");
|
|
3417
|
-
}
|
|
3418
|
-
const { environment, profile, credentialProvision } = parsed;
|
|
3419
|
-
if (credentialProvision) {
|
|
3420
|
-
const [{ createElement: createElement2 }, { render: render2 }, { CredentialProvisionApp: CredentialProvisionApp2 }] = await Promise.all([
|
|
3421
|
-
import("react"),
|
|
3422
|
-
import("ink"),
|
|
3423
|
-
Promise.resolve().then(() => (init_credential_app(), credential_app_exports))
|
|
3424
|
-
]);
|
|
3425
|
-
const app2 = render2(createElement2(CredentialProvisionApp2, { store: new CliProfileStore() }));
|
|
3426
|
-
await app2.waitUntilExit();
|
|
3427
|
-
return;
|
|
3428
|
-
}
|
|
3429
|
-
const store = new CliProfileStore();
|
|
3430
|
-
const actions = createOperatorSessionActions({
|
|
3431
|
-
target,
|
|
3432
|
-
profile,
|
|
3433
|
-
store,
|
|
3434
|
-
timeoutMs: 3e4
|
|
3435
|
-
});
|
|
3436
|
-
const client = await actions.getApi();
|
|
3437
|
-
const [{ createElement }, { render }, { LoginApp: LoginApp2 }] = await Promise.all([
|
|
3438
|
-
import("react"),
|
|
3439
|
-
import("ink"),
|
|
3440
|
-
Promise.resolve().then(() => (init_app(), app_exports))
|
|
3441
|
-
]);
|
|
3442
|
-
const api = {
|
|
3443
|
-
setActionSignal(signal) {
|
|
3444
|
-
client.setActionSignal(signal);
|
|
3445
|
-
},
|
|
3446
|
-
async unlock(username, password, signal) {
|
|
3447
|
-
await actions.loginWithPassword(username, password, signal);
|
|
3448
|
-
},
|
|
3449
|
-
startLogin: () => client.startLogin(),
|
|
3450
|
-
submitOtp: (attemptId, otp) => client.submitOtp(attemptId, otp),
|
|
3451
|
-
watchLoginAttempt: (attemptId, onEvent, signal) => client.watchLoginAttempt(attemptId, onEvent, signal),
|
|
3452
|
-
confirmZerodhaSession: () => client.confirmZerodhaSession()
|
|
3453
|
-
};
|
|
3454
|
-
const app = render(createElement(LoginApp2, {
|
|
3455
|
-
api,
|
|
3456
|
-
expectedOperatorId: target.operatorId,
|
|
3457
|
-
actionTimeoutMs: 3e4,
|
|
3458
|
-
async onResumeSession() {
|
|
3459
|
-
return actions.resumeInteractiveSession();
|
|
3460
|
-
},
|
|
3461
|
-
onReportOperatorActivity: () => actions.reportOperatorActivity(),
|
|
3462
|
-
async onStatus() {
|
|
3463
|
-
const status = await actions.status();
|
|
3464
|
-
return `Operator Session valid: ${status.observedOperator}.`;
|
|
3465
|
-
},
|
|
3466
|
-
async onValidate() {
|
|
3467
|
-
const result = await runOperatorSessionValidation({
|
|
3468
|
-
target,
|
|
3469
|
-
callerProfile: profile,
|
|
3470
|
-
store,
|
|
3471
|
-
timeoutMs: 12e4,
|
|
3472
|
-
cleanupTimeoutMs: 1e4
|
|
3473
|
-
});
|
|
3474
|
-
return result;
|
|
3475
|
-
},
|
|
3476
|
-
onValidateZerodha: (input) => runZerodhaValidation({
|
|
3477
|
-
target,
|
|
3478
|
-
callerProfile: profile,
|
|
3479
|
-
store,
|
|
3480
|
-
timeoutMs: 12e4,
|
|
3481
|
-
signal: input.signal,
|
|
3482
|
-
attemptId: input.attemptId,
|
|
3483
|
-
onObservation: input.onObservation,
|
|
3484
|
-
...input.otp ? { readOtp: async () => input.otp } : {}
|
|
3485
|
-
}),
|
|
3486
|
-
onLogout: () => actions.logout()
|
|
3487
|
-
}));
|
|
3488
|
-
await app.waitUntilExit();
|
|
3489
|
-
}
|
|
3490
|
-
function parseInteractiveArguments(argv) {
|
|
3491
|
-
if (argv[1] === "credential" && argv[2] === "provision") {
|
|
3492
|
-
let environment2 = "";
|
|
3493
|
-
let profile2 = "default";
|
|
3494
|
-
const values2 = /* @__PURE__ */ new Set();
|
|
3495
|
-
for (let index = 3; index < argv.length; index += 2) {
|
|
3496
|
-
const flag = argv[index];
|
|
3497
|
-
const value = argv[index + 1];
|
|
3498
|
-
if (!["--env", "--profile"].includes(flag) || value === void 0 || value.startsWith("--")) {
|
|
3499
|
-
throw new Error(`Invalid interactive argument: ${flag ?? ""}`.trim());
|
|
3500
|
-
}
|
|
3501
|
-
if (values2.has(flag)) throw new Error(`Argument ${flag} may be specified only once.`);
|
|
3502
|
-
values2.add(flag);
|
|
3503
|
-
if (flag === "--env") environment2 = value;
|
|
3504
|
-
if (flag === "--profile") profile2 = value;
|
|
3505
|
-
}
|
|
3506
|
-
assertClientProfileName(profile2);
|
|
3507
|
-
return { environment: values2.has("--env") ? environment2 : "local", profile: profile2, credentialProvision: true };
|
|
3508
|
-
}
|
|
3509
|
-
let environment = "";
|
|
3510
|
-
let profile = "default";
|
|
3511
|
-
const values = /* @__PURE__ */ new Set();
|
|
3512
|
-
for (let index = 1; index < argv.length; index += 2) {
|
|
3513
|
-
const flag = argv[index];
|
|
3514
|
-
const value = argv[index + 1];
|
|
3515
|
-
if (!["--env", "--profile"].includes(flag) || value === void 0 || value.startsWith("--")) {
|
|
3516
|
-
throw new Error(`Invalid interactive argument: ${flag ?? ""}`.trim());
|
|
3517
|
-
}
|
|
3518
|
-
if (values.has(flag)) throw new Error(`Argument ${flag} may be specified only once.`);
|
|
3519
|
-
values.add(flag);
|
|
3520
|
-
if (flag === "--env") environment = value;
|
|
3521
|
-
if (flag === "--profile") profile = value;
|
|
3522
|
-
}
|
|
3523
|
-
assertClientProfileName(profile);
|
|
3524
|
-
return { environment: values.has("--env") ? environment : "local", profile, credentialProvision: false };
|
|
3525
|
-
}
|
|
3526
|
-
var invokedFile = process.argv[1];
|
|
3527
|
-
if (invokedFile && path4.resolve(fileURLToPath2(import.meta.url)) === realpathSync(invokedFile)) {
|
|
3528
|
-
ensureFetchIgnoresEnvProxy();
|
|
3529
|
-
bootstrapCli().catch((error) => {
|
|
3530
|
-
const message = error instanceof Error ? error.message : "CLI failed to start.";
|
|
3531
|
-
process.stderr.write(`${message}
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
var MINIMUM_NODE_MAJOR = 22;
|
|
5
|
+
var found = process.versions.node;
|
|
6
|
+
if (Number(found.split(".")[0]) < MINIMUM_NODE_MAJOR) {
|
|
7
|
+
process.stderr.write(`lmt needs Node ${MINIMUM_NODE_MAJOR} or newer (found ${found}). Install it with: nvm install 24
|
|
3532
8
|
`);
|
|
3533
|
-
|
|
3534
|
-
});
|
|
9
|
+
process.exit(1);
|
|
3535
10
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
normalizeArgv,
|
|
3539
|
-
parseInteractiveArguments
|
|
3540
|
-
};
|
|
11
|
+
var { runCliMain } = await import("./src-MZYWVU46.js");
|
|
12
|
+
runCliMain();
|