@ih8e/express-cli 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.js +612 -356
- package/dist/index.js.map +1 -1
- package/package.json +22 -1
package/dist/index.js
CHANGED
|
@@ -49,6 +49,219 @@ var init_esm_shims = __esm({
|
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
// src/types/express.ts
|
|
53
|
+
var init_express = __esm({
|
|
54
|
+
"src/types/express.ts"() {
|
|
55
|
+
"use strict";
|
|
56
|
+
init_esm_shims();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// src/types/api.ts
|
|
61
|
+
var init_api = __esm({
|
|
62
|
+
"src/types/api.ts"() {
|
|
63
|
+
"use strict";
|
|
64
|
+
init_esm_shims();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// src/types/config.ts
|
|
69
|
+
import { z } from "zod";
|
|
70
|
+
var configSchema, envSchema;
|
|
71
|
+
var init_config = __esm({
|
|
72
|
+
"src/types/config.ts"() {
|
|
73
|
+
"use strict";
|
|
74
|
+
init_esm_shims();
|
|
75
|
+
configSchema = z.object({
|
|
76
|
+
host: z.string().min(1, "host is required \u2014 set EXPRESS_HOST or configure via `express config set host <host>`"),
|
|
77
|
+
protocol: z.enum(["https", "http"]).default("https"),
|
|
78
|
+
token: z.string().optional(),
|
|
79
|
+
locale: z.string().default("ru"),
|
|
80
|
+
platform: z.string().default("web"),
|
|
81
|
+
platform_package_id: z.string().default("ru.alfabank"),
|
|
82
|
+
app_version: z.string().default("3.66.47"),
|
|
83
|
+
output: z.enum(["table", "json"]).default("table")
|
|
84
|
+
});
|
|
85
|
+
envSchema = z.object({
|
|
86
|
+
EXPRESS_HOST: z.string().optional(),
|
|
87
|
+
EXPRESS_TOKEN: z.string().optional(),
|
|
88
|
+
EXPRESS_LOCALE: z.string().optional(),
|
|
89
|
+
EXPRESS_OUTPUT: z.enum(["table", "json"]).optional()
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// src/types/index.ts
|
|
95
|
+
var init_types = __esm({
|
|
96
|
+
"src/types/index.ts"() {
|
|
97
|
+
"use strict";
|
|
98
|
+
init_esm_shims();
|
|
99
|
+
init_express();
|
|
100
|
+
init_api();
|
|
101
|
+
init_config();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// src/config/store.ts
|
|
106
|
+
import Conf from "conf";
|
|
107
|
+
function getStoredConfig() {
|
|
108
|
+
return store.get("config") ?? {};
|
|
109
|
+
}
|
|
110
|
+
function setStoredConfig(partial) {
|
|
111
|
+
const current = getStoredConfig();
|
|
112
|
+
store.set("config", { ...current, ...partial });
|
|
113
|
+
}
|
|
114
|
+
function getAuthToken() {
|
|
115
|
+
return store.get("authToken") ?? null;
|
|
116
|
+
}
|
|
117
|
+
function setAuthToken(token) {
|
|
118
|
+
if (token === null) {
|
|
119
|
+
store.delete("authToken");
|
|
120
|
+
} else {
|
|
121
|
+
store.set("authToken", token);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function getRtsAuthToken() {
|
|
125
|
+
return store.get("rtsAuthToken") ?? null;
|
|
126
|
+
}
|
|
127
|
+
function setRtsAuthToken(token) {
|
|
128
|
+
if (token === null) {
|
|
129
|
+
store.delete("rtsAuthToken");
|
|
130
|
+
} else {
|
|
131
|
+
store.set("rtsAuthToken", token);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function getRefreshToken() {
|
|
135
|
+
return store.get("refreshToken") ?? null;
|
|
136
|
+
}
|
|
137
|
+
function setRefreshToken(token) {
|
|
138
|
+
if (token === null) {
|
|
139
|
+
store.delete("refreshToken");
|
|
140
|
+
} else {
|
|
141
|
+
store.set("refreshToken", token);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function getTokenExpiresAt() {
|
|
145
|
+
return store.get("tokenExpiresAt") ?? null;
|
|
146
|
+
}
|
|
147
|
+
function setTokenExpiresAt(expiresAt) {
|
|
148
|
+
if (expiresAt === null) {
|
|
149
|
+
store.delete("tokenExpiresAt");
|
|
150
|
+
} else {
|
|
151
|
+
store.set("tokenExpiresAt", expiresAt);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function calcTokenExpiresAt(expiresIn) {
|
|
155
|
+
return Date.now() + Math.floor(expiresIn / 2) * 1e3;
|
|
156
|
+
}
|
|
157
|
+
function isTokenExpiringSoon() {
|
|
158
|
+
const expiresAt = getTokenExpiresAt();
|
|
159
|
+
if (!expiresAt) return true;
|
|
160
|
+
return Date.now() >= expiresAt;
|
|
161
|
+
}
|
|
162
|
+
function getEtsAuthToken() {
|
|
163
|
+
return store.get("etsAuthToken") ?? null;
|
|
164
|
+
}
|
|
165
|
+
function setEtsAuthToken(token) {
|
|
166
|
+
if (token === null) {
|
|
167
|
+
store.delete("etsAuthToken");
|
|
168
|
+
} else {
|
|
169
|
+
store.set("etsAuthToken", token);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function getApigwKeysRaw() {
|
|
173
|
+
return store.get("apigwKeys") ?? null;
|
|
174
|
+
}
|
|
175
|
+
function setApigwKeysRaw(data) {
|
|
176
|
+
if (data === null) {
|
|
177
|
+
store.delete("apigwKeys");
|
|
178
|
+
} else {
|
|
179
|
+
store.set("apigwKeys", data);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function clearAll() {
|
|
183
|
+
store.clear();
|
|
184
|
+
}
|
|
185
|
+
var store;
|
|
186
|
+
var init_store = __esm({
|
|
187
|
+
"src/config/store.ts"() {
|
|
188
|
+
"use strict";
|
|
189
|
+
init_esm_shims();
|
|
190
|
+
store = new Conf({
|
|
191
|
+
projectName: "express-cli",
|
|
192
|
+
defaults: {
|
|
193
|
+
config: {},
|
|
194
|
+
authToken: null,
|
|
195
|
+
refreshToken: null,
|
|
196
|
+
rtsAuthToken: null,
|
|
197
|
+
apigwKeys: null,
|
|
198
|
+
tokenExpiresAt: null,
|
|
199
|
+
etsAuthToken: null
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// src/config/loader.ts
|
|
206
|
+
var loader_exports = {};
|
|
207
|
+
__export(loader_exports, {
|
|
208
|
+
getBaseUrl: () => getBaseUrl,
|
|
209
|
+
getEtsBaseUrl: () => getEtsBaseUrl,
|
|
210
|
+
getWebOrigin: () => getWebOrigin,
|
|
211
|
+
loadConfig: () => loadConfig
|
|
212
|
+
});
|
|
213
|
+
import { ZodError } from "zod";
|
|
214
|
+
function loadConfig(cliOverrides = {}) {
|
|
215
|
+
const stored = getStoredConfig();
|
|
216
|
+
const env2 = {};
|
|
217
|
+
if (process.env.EXPRESS_HOST) env2.host = process.env.EXPRESS_HOST;
|
|
218
|
+
if (process.env.EXPRESS_TOKEN) env2.token = process.env.EXPRESS_TOKEN;
|
|
219
|
+
if (process.env.EXPRESS_LOCALE) env2.locale = process.env.EXPRESS_LOCALE;
|
|
220
|
+
if (process.env.EXPRESS_OUTPUT) env2.output = process.env.EXPRESS_OUTPUT;
|
|
221
|
+
const merged = {
|
|
222
|
+
...stored,
|
|
223
|
+
...env2,
|
|
224
|
+
...cliOverrides
|
|
225
|
+
};
|
|
226
|
+
if (!merged.token) {
|
|
227
|
+
const storedToken = getAuthToken();
|
|
228
|
+
if (storedToken) merged.token = storedToken;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
return configSchema.parse(merged);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
if (err instanceof ZodError) {
|
|
234
|
+
const details = err.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
235
|
+
throw new Error(`Configuration error:
|
|
236
|
+
${details}
|
|
237
|
+
|
|
238
|
+
Run: express-cli config set host <hostname>`);
|
|
239
|
+
}
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function getBaseUrl(config) {
|
|
244
|
+
return `${config.protocol}://${config.host}`;
|
|
245
|
+
}
|
|
246
|
+
function getDomain(ctsHost) {
|
|
247
|
+
const parts = ctsHost.split(".");
|
|
248
|
+
return parts.length > 2 ? parts.slice(1).join(".") : ctsHost;
|
|
249
|
+
}
|
|
250
|
+
function getEtsBaseUrl(config) {
|
|
251
|
+
return `https://ets.${getDomain(config.host)}`;
|
|
252
|
+
}
|
|
253
|
+
function getWebOrigin(config) {
|
|
254
|
+
return `https://${getDomain(config.host)}`;
|
|
255
|
+
}
|
|
256
|
+
var init_loader = __esm({
|
|
257
|
+
"src/config/loader.ts"() {
|
|
258
|
+
"use strict";
|
|
259
|
+
init_esm_shims();
|
|
260
|
+
init_types();
|
|
261
|
+
init_store();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
52
265
|
// node_modules/react/cjs/react.production.js
|
|
53
266
|
var require_react_production = __commonJS({
|
|
54
267
|
"node_modules/react/cjs/react.production.js"(exports) {
|
|
@@ -25062,179 +25275,11 @@ init_esm_shims();
|
|
|
25062
25275
|
|
|
25063
25276
|
// src/config/index.ts
|
|
25064
25277
|
init_esm_shims();
|
|
25065
|
-
|
|
25066
|
-
|
|
25067
|
-
init_esm_shims();
|
|
25068
|
-
import { ZodError } from "zod";
|
|
25069
|
-
|
|
25070
|
-
// src/types/index.ts
|
|
25071
|
-
init_esm_shims();
|
|
25072
|
-
|
|
25073
|
-
// src/types/express.ts
|
|
25074
|
-
init_esm_shims();
|
|
25075
|
-
|
|
25076
|
-
// src/types/api.ts
|
|
25077
|
-
init_esm_shims();
|
|
25078
|
-
|
|
25079
|
-
// src/types/config.ts
|
|
25080
|
-
init_esm_shims();
|
|
25081
|
-
import { z } from "zod";
|
|
25082
|
-
var configSchema = z.object({
|
|
25083
|
-
host: z.string().min(1, "host is required \u2014 set EXPRESS_HOST or configure via `express config set host <host>`"),
|
|
25084
|
-
protocol: z.enum(["https", "http"]).default("https"),
|
|
25085
|
-
token: z.string().optional(),
|
|
25086
|
-
locale: z.string().default("ru"),
|
|
25087
|
-
platform: z.string().default("web"),
|
|
25088
|
-
platform_package_id: z.string().default("ru.alfabank"),
|
|
25089
|
-
app_version: z.string().default("3.66.47"),
|
|
25090
|
-
output: z.enum(["table", "json"]).default("table")
|
|
25091
|
-
});
|
|
25092
|
-
var envSchema = z.object({
|
|
25093
|
-
EXPRESS_HOST: z.string().optional(),
|
|
25094
|
-
EXPRESS_TOKEN: z.string().optional(),
|
|
25095
|
-
EXPRESS_LOCALE: z.string().optional(),
|
|
25096
|
-
EXPRESS_OUTPUT: z.enum(["table", "json"]).optional()
|
|
25097
|
-
});
|
|
25098
|
-
|
|
25099
|
-
// src/config/store.ts
|
|
25100
|
-
init_esm_shims();
|
|
25101
|
-
import Conf from "conf";
|
|
25102
|
-
var store = new Conf({
|
|
25103
|
-
projectName: "express-cli",
|
|
25104
|
-
defaults: {
|
|
25105
|
-
config: {},
|
|
25106
|
-
authToken: null,
|
|
25107
|
-
refreshToken: null,
|
|
25108
|
-
rtsAuthToken: null,
|
|
25109
|
-
apigwKeys: null,
|
|
25110
|
-
tokenExpiresAt: null,
|
|
25111
|
-
etsAuthToken: null
|
|
25112
|
-
}
|
|
25113
|
-
});
|
|
25114
|
-
function getStoredConfig() {
|
|
25115
|
-
return store.get("config") ?? {};
|
|
25116
|
-
}
|
|
25117
|
-
function setStoredConfig(partial) {
|
|
25118
|
-
const current = getStoredConfig();
|
|
25119
|
-
store.set("config", { ...current, ...partial });
|
|
25120
|
-
}
|
|
25121
|
-
function getAuthToken() {
|
|
25122
|
-
return store.get("authToken") ?? null;
|
|
25123
|
-
}
|
|
25124
|
-
function setAuthToken(token) {
|
|
25125
|
-
if (token === null) {
|
|
25126
|
-
store.delete("authToken");
|
|
25127
|
-
} else {
|
|
25128
|
-
store.set("authToken", token);
|
|
25129
|
-
}
|
|
25130
|
-
}
|
|
25131
|
-
function getRtsAuthToken() {
|
|
25132
|
-
return store.get("rtsAuthToken") ?? null;
|
|
25133
|
-
}
|
|
25134
|
-
function setRtsAuthToken(token) {
|
|
25135
|
-
if (token === null) {
|
|
25136
|
-
store.delete("rtsAuthToken");
|
|
25137
|
-
} else {
|
|
25138
|
-
store.set("rtsAuthToken", token);
|
|
25139
|
-
}
|
|
25140
|
-
}
|
|
25141
|
-
function getRefreshToken() {
|
|
25142
|
-
return store.get("refreshToken") ?? null;
|
|
25143
|
-
}
|
|
25144
|
-
function setRefreshToken(token) {
|
|
25145
|
-
if (token === null) {
|
|
25146
|
-
store.delete("refreshToken");
|
|
25147
|
-
} else {
|
|
25148
|
-
store.set("refreshToken", token);
|
|
25149
|
-
}
|
|
25150
|
-
}
|
|
25151
|
-
function getTokenExpiresAt() {
|
|
25152
|
-
return store.get("tokenExpiresAt") ?? null;
|
|
25153
|
-
}
|
|
25154
|
-
function setTokenExpiresAt(expiresAt) {
|
|
25155
|
-
if (expiresAt === null) {
|
|
25156
|
-
store.delete("tokenExpiresAt");
|
|
25157
|
-
} else {
|
|
25158
|
-
store.set("tokenExpiresAt", expiresAt);
|
|
25159
|
-
}
|
|
25160
|
-
}
|
|
25161
|
-
function calcTokenExpiresAt(expiresIn) {
|
|
25162
|
-
return Date.now() + Math.floor(expiresIn / 2) * 1e3;
|
|
25163
|
-
}
|
|
25164
|
-
function isTokenExpiringSoon() {
|
|
25165
|
-
const expiresAt = getTokenExpiresAt();
|
|
25166
|
-
if (!expiresAt) return true;
|
|
25167
|
-
return Date.now() >= expiresAt;
|
|
25168
|
-
}
|
|
25169
|
-
function getEtsAuthToken() {
|
|
25170
|
-
return store.get("etsAuthToken") ?? null;
|
|
25171
|
-
}
|
|
25172
|
-
function setEtsAuthToken(token) {
|
|
25173
|
-
if (token === null) {
|
|
25174
|
-
store.delete("etsAuthToken");
|
|
25175
|
-
} else {
|
|
25176
|
-
store.set("etsAuthToken", token);
|
|
25177
|
-
}
|
|
25178
|
-
}
|
|
25179
|
-
function getApigwKeysRaw() {
|
|
25180
|
-
return store.get("apigwKeys") ?? null;
|
|
25181
|
-
}
|
|
25182
|
-
function setApigwKeysRaw(data) {
|
|
25183
|
-
if (data === null) {
|
|
25184
|
-
store.delete("apigwKeys");
|
|
25185
|
-
} else {
|
|
25186
|
-
store.set("apigwKeys", data);
|
|
25187
|
-
}
|
|
25188
|
-
}
|
|
25189
|
-
function clearAll() {
|
|
25190
|
-
store.clear();
|
|
25191
|
-
}
|
|
25192
|
-
|
|
25193
|
-
// src/config/loader.ts
|
|
25194
|
-
function loadConfig(cliOverrides = {}) {
|
|
25195
|
-
const stored = getStoredConfig();
|
|
25196
|
-
const env2 = {};
|
|
25197
|
-
if (process.env.EXPRESS_HOST) env2.host = process.env.EXPRESS_HOST;
|
|
25198
|
-
if (process.env.EXPRESS_TOKEN) env2.token = process.env.EXPRESS_TOKEN;
|
|
25199
|
-
if (process.env.EXPRESS_LOCALE) env2.locale = process.env.EXPRESS_LOCALE;
|
|
25200
|
-
if (process.env.EXPRESS_OUTPUT) env2.output = process.env.EXPRESS_OUTPUT;
|
|
25201
|
-
const merged = {
|
|
25202
|
-
...stored,
|
|
25203
|
-
...env2,
|
|
25204
|
-
...cliOverrides
|
|
25205
|
-
};
|
|
25206
|
-
if (!merged.token) {
|
|
25207
|
-
const storedToken = getAuthToken();
|
|
25208
|
-
if (storedToken) merged.token = storedToken;
|
|
25209
|
-
}
|
|
25210
|
-
try {
|
|
25211
|
-
return configSchema.parse(merged);
|
|
25212
|
-
} catch (err) {
|
|
25213
|
-
if (err instanceof ZodError) {
|
|
25214
|
-
const details = err.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
25215
|
-
throw new Error(`Configuration error:
|
|
25216
|
-
${details}
|
|
25217
|
-
|
|
25218
|
-
Run: express-cli config set host <hostname>`);
|
|
25219
|
-
}
|
|
25220
|
-
throw err;
|
|
25221
|
-
}
|
|
25222
|
-
}
|
|
25223
|
-
function getBaseUrl(config) {
|
|
25224
|
-
return `${config.protocol}://${config.host}`;
|
|
25225
|
-
}
|
|
25226
|
-
function getDomain(ctsHost) {
|
|
25227
|
-
const parts = ctsHost.split(".");
|
|
25228
|
-
return parts.length > 2 ? parts.slice(1).join(".") : ctsHost;
|
|
25229
|
-
}
|
|
25230
|
-
function getEtsBaseUrl(config) {
|
|
25231
|
-
return `https://ets.${getDomain(config.host)}`;
|
|
25232
|
-
}
|
|
25233
|
-
function getWebOrigin(config) {
|
|
25234
|
-
return `https://${getDomain(config.host)}`;
|
|
25235
|
-
}
|
|
25278
|
+
init_loader();
|
|
25279
|
+
init_store();
|
|
25236
25280
|
|
|
25237
25281
|
// src/auth/import.ts
|
|
25282
|
+
init_loader();
|
|
25238
25283
|
async function importToken(token, cliOverrides = {}) {
|
|
25239
25284
|
const config = loadConfig(cliOverrides);
|
|
25240
25285
|
const baseUrl = getBaseUrl(config);
|
|
@@ -25275,6 +25320,7 @@ init_esm_shims();
|
|
|
25275
25320
|
|
|
25276
25321
|
// src/auth/keys.ts
|
|
25277
25322
|
init_esm_shims();
|
|
25323
|
+
init_store();
|
|
25278
25324
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
25279
25325
|
import { randomBytes } from "crypto";
|
|
25280
25326
|
import nacl from "tweetnacl";
|
|
@@ -25561,6 +25607,8 @@ ${signingString}`);
|
|
|
25561
25607
|
}
|
|
25562
25608
|
|
|
25563
25609
|
// src/auth/device-login.ts
|
|
25610
|
+
init_store();
|
|
25611
|
+
init_loader();
|
|
25564
25612
|
import nacl2 from "tweetnacl";
|
|
25565
25613
|
import { randomUUID } from "crypto";
|
|
25566
25614
|
async function fetchCurrentAccountCtsKey(baseUrl, token, userHuid, webOrigin) {
|
|
@@ -25958,6 +26006,30 @@ Minting a new one would break your other devices. Import the shared key instead:
|
|
|
25958
26006
|
|
|
25959
26007
|
// src/auth/qr-login.ts
|
|
25960
26008
|
init_esm_shims();
|
|
26009
|
+
|
|
26010
|
+
// src/auth/qr-browser.ts
|
|
26011
|
+
init_esm_shims();
|
|
26012
|
+
import { execFile } from "child_process";
|
|
26013
|
+
import QRCode from "qrcode";
|
|
26014
|
+
async function openQrInBrowser(payload, registrationId) {
|
|
26015
|
+
let pngDataUrl;
|
|
26016
|
+
try {
|
|
26017
|
+
pngDataUrl = await QRCode.toDataURL(payload, { scale: 8, margin: 2 });
|
|
26018
|
+
} catch {
|
|
26019
|
+
return;
|
|
26020
|
+
}
|
|
26021
|
+
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>eXpress QR</title><style>body{font-family:sans-serif;background:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0}h2{color:#1a1a1a;margin-bottom:16px}img{width:280px;height:280px;image-rendering:pixelated;border:1px solid #eee;border-radius:8px}p{color:#999;font-size:11px;margin-top:12px;font-family:monospace}</style></head><body><h2>Scan with eXpress</h2><img src="${pngDataUrl}" alt="QR"><p>${registrationId}</p></body></html>`;
|
|
26022
|
+
const htmlDataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
26023
|
+
try {
|
|
26024
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
26025
|
+
execFile(opener, [htmlDataUrl]);
|
|
26026
|
+
} catch {
|
|
26027
|
+
}
|
|
26028
|
+
}
|
|
26029
|
+
|
|
26030
|
+
// src/auth/qr-login.ts
|
|
26031
|
+
init_store();
|
|
26032
|
+
init_loader();
|
|
25961
26033
|
import { randomUUID as randomUUID2, randomBytes as randomBytes2 } from "crypto";
|
|
25962
26034
|
import qrcode from "qrcode-terminal";
|
|
25963
26035
|
import nacl3 from "tweetnacl";
|
|
@@ -25992,16 +26064,20 @@ function commonHeaders(webOrigin) {
|
|
|
25992
26064
|
"sec-ch-ua-platform": '"macOS"'
|
|
25993
26065
|
};
|
|
25994
26066
|
}
|
|
25995
|
-
|
|
26067
|
+
function buildQrMaterial(cliOverrides = {}) {
|
|
25996
26068
|
const config = loadConfig(cliOverrides);
|
|
25997
|
-
const etsBaseUrl = getEtsBaseUrl(config);
|
|
25998
|
-
const webOrigin = getWebOrigin(config);
|
|
25999
26069
|
const qrSigningKey = generateSigningKeyPair();
|
|
26000
26070
|
const registrationId = qrSigningKey.keyId;
|
|
26001
26071
|
const registrationToken = Buffer.from(randomBytes2(64)).toString("base64");
|
|
26002
26072
|
const signPubKey = publicKeyToBase64(qrSigningKey.publicKey);
|
|
26003
26073
|
const udid = randomUUID2();
|
|
26004
26074
|
const encryptionKey = randomBytes2(32);
|
|
26075
|
+
const qrPayload = JSON.stringify({
|
|
26076
|
+
registration_id: registrationId,
|
|
26077
|
+
registration_token: registrationToken,
|
|
26078
|
+
registration_key: Buffer.from(encryptionKey).toString("base64"),
|
|
26079
|
+
version: 1
|
|
26080
|
+
});
|
|
26005
26081
|
const qrBody = JSON.stringify({
|
|
26006
26082
|
registration_id: registrationId,
|
|
26007
26083
|
registration_token: registrationToken,
|
|
@@ -26021,63 +26097,53 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26021
26097
|
platform: "web",
|
|
26022
26098
|
platform_package_id: "com.pyligrim.alphach"
|
|
26023
26099
|
});
|
|
26024
|
-
|
|
26025
|
-
|
|
26026
|
-
|
|
26027
|
-
|
|
26028
|
-
|
|
26029
|
-
});
|
|
26030
|
-
console.log("Step 1/7: Scan this QR code with your eXpress app:\n");
|
|
26031
|
-
qrcode.generate(qrPayload, { small: true }, (qr) => {
|
|
26032
|
-
console.log(qr);
|
|
26033
|
-
});
|
|
26034
|
-
console.log(`
|
|
26035
|
-
registration_id: ${registrationId}`);
|
|
26036
|
-
console.log(" Waiting for scan (server long-polling)...\n");
|
|
26100
|
+
return { registrationId, encryptionKey, qrSigningKey, udid, qrPayload, qrBody, config };
|
|
26101
|
+
}
|
|
26102
|
+
async function pollForQrScan(mat) {
|
|
26103
|
+
const etsBaseUrl = getEtsBaseUrl(mat.config);
|
|
26104
|
+
const webOrigin = getWebOrigin(mat.config);
|
|
26037
26105
|
const etsUrl = `${etsBaseUrl}/api/v1/authentication/qr/mobile_to_web/request`;
|
|
26038
26106
|
const qrHeaders = signQrRequest({
|
|
26039
26107
|
method: "POST",
|
|
26040
26108
|
url: etsUrl,
|
|
26041
|
-
body: qrBody,
|
|
26042
|
-
registrationId,
|
|
26043
|
-
privateKey: qrSigningKey.privateKey
|
|
26109
|
+
body: mat.qrBody,
|
|
26110
|
+
registrationId: mat.registrationId,
|
|
26111
|
+
privateKey: mat.qrSigningKey.privateKey
|
|
26044
26112
|
});
|
|
26045
|
-
let
|
|
26113
|
+
let res;
|
|
26046
26114
|
try {
|
|
26047
|
-
|
|
26115
|
+
res = await fetch(etsUrl, {
|
|
26048
26116
|
method: "POST",
|
|
26049
26117
|
headers: { ...commonHeaders(webOrigin), ...qrHeaders },
|
|
26050
|
-
body: qrBody
|
|
26118
|
+
body: mat.qrBody
|
|
26051
26119
|
});
|
|
26052
26120
|
} catch (err) {
|
|
26053
26121
|
throw new Error(`QR request network error: ${err.message}`);
|
|
26054
26122
|
}
|
|
26055
|
-
const
|
|
26056
|
-
if (!
|
|
26057
|
-
|
|
26058
|
-
|
|
26059
|
-
|
|
26060
|
-
|
|
26061
|
-
|
|
26062
|
-
|
|
26063
|
-
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
if (process.env.EXPRESS_DEBUG) {
|
|
26067
|
-
console.log(` [DEBUG] QR full response: ${qrText.slice(0, 1e3)}`);
|
|
26123
|
+
const text = await res.text();
|
|
26124
|
+
if (!res.ok) throw new Error(`QR request failed (${res.status}): ${text.slice(0, 500)}`);
|
|
26125
|
+
const data = JSON.parse(text);
|
|
26126
|
+
if (process.env.EXPRESS_DEBUG) process.stderr.write(`[qr] full response: ${text.slice(0, 1e3)}
|
|
26127
|
+
`);
|
|
26128
|
+
const result = extractResult2(data);
|
|
26129
|
+
const ctsRegistrationToken = result.cts_registration_token ?? "";
|
|
26130
|
+
const rtsRegistrationToken = result.rts_registration_token ?? "";
|
|
26131
|
+
const registrationData = result.registration_data ?? "";
|
|
26132
|
+
if (!ctsRegistrationToken && !rtsRegistrationToken) {
|
|
26133
|
+
throw new Error(`No tokens in QR response: ${text.slice(0, 500)}`);
|
|
26068
26134
|
}
|
|
26069
|
-
|
|
26070
|
-
|
|
26071
|
-
|
|
26072
|
-
const
|
|
26073
|
-
|
|
26135
|
+
return { ctsRegistrationToken, rtsRegistrationToken, registrationData };
|
|
26136
|
+
}
|
|
26137
|
+
async function completeQrRegistration(mat, poll, log = console.log) {
|
|
26138
|
+
const { config, registrationId, encryptionKey, qrSigningKey } = mat;
|
|
26139
|
+
const { ctsRegistrationToken, rtsRegistrationToken, registrationData } = poll;
|
|
26140
|
+
const etsBaseUrl = getEtsBaseUrl(config);
|
|
26141
|
+
const webOrigin = getWebOrigin(config);
|
|
26142
|
+
log(" QR scanned! Got tokens from server.");
|
|
26074
26143
|
if (process.env.EXPRESS_DEBUG) {
|
|
26075
|
-
|
|
26076
|
-
|
|
26077
|
-
|
|
26078
|
-
}
|
|
26079
|
-
if (!ctsRegistrationToken && !rtsRegistrationToken) {
|
|
26080
|
-
throw new Error(`No tokens in QR response: ${qrText.slice(0, 500)}`);
|
|
26144
|
+
log(` [DEBUG] registration_data length: ${registrationData.length}`);
|
|
26145
|
+
log(` [DEBUG] registration_data raw: ${registrationData.slice(0, 100)}...`);
|
|
26146
|
+
log(` [DEBUG] encryptionKey (registration_key) hex: ${Buffer.from(encryptionKey).toString("hex")}`);
|
|
26081
26147
|
}
|
|
26082
26148
|
let rtsPrivateKey = null;
|
|
26083
26149
|
let rtsPublicKeyId = "";
|
|
@@ -26087,14 +26153,14 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26087
26153
|
try {
|
|
26088
26154
|
const raw = Uint8Array.from(Buffer.from(registrationData, "base64"));
|
|
26089
26155
|
if (process.env.EXPRESS_DEBUG) {
|
|
26090
|
-
|
|
26091
|
-
|
|
26092
|
-
|
|
26093
|
-
|
|
26156
|
+
log(` [DEBUG] registration_data decoded length: ${raw.length}`);
|
|
26157
|
+
log(` [DEBUG] first 40 bytes hex: ${Buffer.from(raw.slice(0, 40)).toString("hex")}`);
|
|
26158
|
+
log(` [DEBUG] encryptionKey hex: ${Buffer.from(encryptionKey).toString("hex")}`);
|
|
26159
|
+
log(` [DEBUG] encryptionKey length: ${encryptionKey.length}`);
|
|
26094
26160
|
}
|
|
26095
26161
|
const decrypted = decryptRegistrationData(registrationData, encryptionKey);
|
|
26096
26162
|
if (process.env.EXPRESS_DEBUG) {
|
|
26097
|
-
|
|
26163
|
+
log(" Decrypted registration_data: " + JSON.stringify(decrypted).slice(0, 500));
|
|
26098
26164
|
}
|
|
26099
26165
|
if (decrypted && typeof decrypted === "object") {
|
|
26100
26166
|
const data = decrypted;
|
|
@@ -26110,10 +26176,10 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26110
26176
|
}
|
|
26111
26177
|
}
|
|
26112
26178
|
} catch (err) {
|
|
26113
|
-
|
|
26179
|
+
log(` Warning: could not decrypt registration_data: ${err.message}`);
|
|
26114
26180
|
}
|
|
26115
26181
|
}
|
|
26116
|
-
|
|
26182
|
+
log("\nStep 2: Confirming with ETS...");
|
|
26117
26183
|
const confirmUrl = `${etsBaseUrl}/api/v1/authentication/register_confirm/qr`;
|
|
26118
26184
|
const confirmBody = JSON.stringify({
|
|
26119
26185
|
registration_id: registrationId,
|
|
@@ -26132,23 +26198,17 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26132
26198
|
body: confirmBody
|
|
26133
26199
|
});
|
|
26134
26200
|
const confirmText = await confirmRes.text();
|
|
26135
|
-
if (!confirmRes.ok) {
|
|
26136
|
-
throw new Error(`ETS register_confirm failed (${confirmRes.status}): ${confirmText.slice(0, 500)}`);
|
|
26137
|
-
}
|
|
26201
|
+
if (!confirmRes.ok) throw new Error(`ETS register_confirm failed (${confirmRes.status}): ${confirmText.slice(0, 500)}`);
|
|
26138
26202
|
const confirmData = extractResult2(JSON.parse(confirmText));
|
|
26139
26203
|
const userHuid = confirmData.user_huid ?? "";
|
|
26140
26204
|
const etsAuthToken = confirmData.auth_token ?? "";
|
|
26141
|
-
|
|
26205
|
+
log(` ETS confirmed. User: ${userHuid || "unknown"}`);
|
|
26142
26206
|
if (etsAuthToken) {
|
|
26143
26207
|
setEtsAuthToken(etsAuthToken);
|
|
26144
|
-
if (process.env.EXPRESS_DEBUG) {
|
|
26145
|
-
console.log(` [DEBUG] ETS auth_token saved (${etsAuthToken.length} chars)`);
|
|
26146
|
-
}
|
|
26208
|
+
if (process.env.EXPRESS_DEBUG) log(` [DEBUG] ETS auth_token saved (${etsAuthToken.length} chars)`);
|
|
26147
26209
|
}
|
|
26148
|
-
if (!ctsRegistrationToken)
|
|
26149
|
-
|
|
26150
|
-
}
|
|
26151
|
-
console.log("\nStep 3/7: Confirming with CTS (AD integration)...");
|
|
26210
|
+
if (!ctsRegistrationToken) throw new Error("No cts_registration_token \u2014 cannot confirm with CTS");
|
|
26211
|
+
log("\nStep 3: Confirming with CTS (AD integration)...");
|
|
26152
26212
|
const ctsUrl = `${getBaseUrl(config)}/api/v1/ad_integration/register_confirm/qr`;
|
|
26153
26213
|
const adConfirmBody = JSON.stringify({
|
|
26154
26214
|
rts_registration_id: registrationId,
|
|
@@ -26168,9 +26228,7 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26168
26228
|
body: adConfirmBody
|
|
26169
26229
|
});
|
|
26170
26230
|
const adText = await adRes.text();
|
|
26171
|
-
if (!adRes.ok) {
|
|
26172
|
-
throw new Error(`AD integration confirm failed (${adRes.status}): ${adText.slice(0, 500)}`);
|
|
26173
|
-
}
|
|
26231
|
+
if (!adRes.ok) throw new Error(`AD integration confirm failed (${adRes.status}): ${adText.slice(0, 500)}`);
|
|
26174
26232
|
const adData = extractResult2(JSON.parse(adText));
|
|
26175
26233
|
const accessToken = adData.access_token;
|
|
26176
26234
|
const refreshToken2 = adData.refresh_token;
|
|
@@ -26179,26 +26237,19 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26179
26237
|
const encryptedRtsToken = adData.encrypted_rts_token;
|
|
26180
26238
|
if (process.env.EXPRESS_DEBUG) {
|
|
26181
26239
|
const adDataRaw = JSON.parse(adText);
|
|
26182
|
-
|
|
26183
|
-
if (encryptedRtsToken) {
|
|
26184
|
-
|
|
26185
|
-
} else {
|
|
26186
|
-
console.log(` [DEBUG] encrypted_rts_token NOT found in response`);
|
|
26187
|
-
}
|
|
26188
|
-
}
|
|
26189
|
-
if (!accessToken) {
|
|
26190
|
-
throw new Error(`No access_token in AD confirm response: ${adText.slice(0, 500)}`);
|
|
26240
|
+
log(` [DEBUG] AD confirm full result keys: ${JSON.stringify(Object.keys(adDataRaw.result || adDataRaw))}`);
|
|
26241
|
+
if (encryptedRtsToken) log(` [DEBUG] encrypted_rts_token found: ${encryptedRtsToken.slice(0, 60)}...`);
|
|
26242
|
+
else log(` [DEBUG] encrypted_rts_token NOT found in response`);
|
|
26191
26243
|
}
|
|
26244
|
+
if (!accessToken) throw new Error(`No access_token in AD confirm response: ${adText.slice(0, 500)}`);
|
|
26192
26245
|
setAuthToken(accessToken);
|
|
26193
|
-
if (refreshToken2)
|
|
26194
|
-
setRefreshToken(refreshToken2);
|
|
26195
|
-
}
|
|
26246
|
+
if (refreshToken2) setRefreshToken(refreshToken2);
|
|
26196
26247
|
if (typeof expiresIn === "number") {
|
|
26197
26248
|
setTokenExpiresAt(calcTokenExpiresAt(expiresIn));
|
|
26198
|
-
|
|
26249
|
+
log(` Token expires in ${expiresIn}s (refresh after ${(expiresIn / 2 / 60).toFixed(0)} min)`);
|
|
26199
26250
|
}
|
|
26200
|
-
|
|
26201
|
-
|
|
26251
|
+
log(` CTS confirmed. Access token: ${accessToken.slice(0, 40)}...`);
|
|
26252
|
+
log("\nStep 4: Registering device token...");
|
|
26202
26253
|
const tokenUrl = `${getBaseUrl(config)}/api/v1/ad_integration/token`;
|
|
26203
26254
|
const tokenBody = JSON.stringify({
|
|
26204
26255
|
app_version: config.app_version,
|
|
@@ -26217,23 +26268,19 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26217
26268
|
});
|
|
26218
26269
|
const tokenRes = await fetch(tokenUrl, {
|
|
26219
26270
|
method: "PUT",
|
|
26220
|
-
headers: {
|
|
26221
|
-
...commonHeaders(webOrigin),
|
|
26222
|
-
Authorization: `Bearer ${accessToken}`,
|
|
26223
|
-
"Content-Type": "application/json"
|
|
26224
|
-
},
|
|
26271
|
+
headers: { ...commonHeaders(webOrigin), Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
26225
26272
|
body: tokenBody
|
|
26226
26273
|
});
|
|
26227
26274
|
if (!tokenRes.ok) {
|
|
26228
26275
|
const tokenErrText = await tokenRes.text().catch(() => "");
|
|
26229
|
-
|
|
26276
|
+
log(` Warning: device token registration failed (${tokenRes.status}): ${tokenErrText.slice(0, 200)}`);
|
|
26230
26277
|
} else {
|
|
26231
|
-
|
|
26278
|
+
log(" Device token registered.");
|
|
26232
26279
|
}
|
|
26233
|
-
|
|
26280
|
+
log("\nStep 5: Registering signing key + fetching server key...");
|
|
26234
26281
|
if (process.env.EXPRESS_DEBUG) {
|
|
26235
|
-
|
|
26236
|
-
|
|
26282
|
+
log(` [DEBUG] serverId: ${serverId}`);
|
|
26283
|
+
log(` [DEBUG] userHuid: ${userHuid}`);
|
|
26237
26284
|
}
|
|
26238
26285
|
const apigwSigningKey = generateSigningKeyPair();
|
|
26239
26286
|
const apigwKeyPublicBase64 = publicKeyToBase64(apigwSigningKey.publicKey);
|
|
@@ -26247,56 +26294,42 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26247
26294
|
const [kdcSignRes, etsKdcSignRes, etsKdcStartRes] = await Promise.all([
|
|
26248
26295
|
fetch(kdcSignUrl, {
|
|
26249
26296
|
method: "POST",
|
|
26250
|
-
headers: {
|
|
26251
|
-
...commonHeaders(webOrigin),
|
|
26252
|
-
Authorization: `Bearer ${accessToken}`,
|
|
26253
|
-
"Content-Type": "application/json"
|
|
26254
|
-
},
|
|
26297
|
+
headers: { ...commonHeaders(webOrigin), Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
26255
26298
|
body: kdcSignBody
|
|
26256
26299
|
}),
|
|
26257
26300
|
fetch(`${etsBaseUrl}/api/v2/kdc/keys/${userHuid}`, {
|
|
26258
26301
|
method: "POST",
|
|
26259
|
-
headers: {
|
|
26260
|
-
...commonHeaders(webOrigin),
|
|
26261
|
-
Authorization: `Bearer ${etsAuthToken}`,
|
|
26262
|
-
"Content-Type": "application/json"
|
|
26263
|
-
},
|
|
26302
|
+
headers: { ...commonHeaders(webOrigin), Authorization: `Bearer ${etsAuthToken}`, "Content-Type": "application/json" },
|
|
26264
26303
|
body: kdcSignBody
|
|
26265
26304
|
}),
|
|
26266
|
-
fetch(`${etsBaseUrl}/api/v1/kdc/start`, {
|
|
26267
|
-
headers: {
|
|
26268
|
-
...commonHeaders(webOrigin)
|
|
26269
|
-
}
|
|
26270
|
-
})
|
|
26305
|
+
fetch(`${etsBaseUrl}/api/v1/kdc/start`, { headers: { ...commonHeaders(webOrigin) } })
|
|
26271
26306
|
]);
|
|
26272
26307
|
if (!kdcSignRes.ok) {
|
|
26273
26308
|
const kdcErrText = await kdcSignRes.text().catch(() => "");
|
|
26274
|
-
|
|
26309
|
+
log(` Warning: CTS KDC signing key registration failed (${kdcSignRes.status}): ${kdcErrText.slice(0, 200)}`);
|
|
26275
26310
|
} else {
|
|
26276
26311
|
const kdcSignData = await kdcSignRes.json().catch(() => null);
|
|
26277
|
-
|
|
26312
|
+
log(` Signing key registered in CTS: ${apigwSigningKey.keyId}` + (kdcSignData ? " " + JSON.stringify(kdcSignData).slice(0, 200) : ""));
|
|
26278
26313
|
}
|
|
26279
26314
|
if (!etsKdcSignRes.ok) {
|
|
26280
26315
|
const etsErrText = await etsKdcSignRes.text().catch(() => "");
|
|
26281
|
-
|
|
26316
|
+
log(` Warning: ETS KDC signing key registration failed (${etsKdcSignRes.status}): ${etsErrText.slice(0, 200)}`);
|
|
26282
26317
|
} else {
|
|
26283
26318
|
const etsSignData = await etsKdcSignRes.json().catch(() => null);
|
|
26284
|
-
|
|
26319
|
+
log(` Signing key registered in ETS: ${apigwSigningKey.keyId}` + (etsSignData ? " " + JSON.stringify(etsSignData).slice(0, 200) : ""));
|
|
26285
26320
|
}
|
|
26286
26321
|
let serverPublicKey = new Uint8Array(0);
|
|
26287
26322
|
let serverPublicKeyId = "";
|
|
26288
26323
|
if (etsKdcStartRes.ok) {
|
|
26289
26324
|
const kdcStartText = await etsKdcStartRes.text();
|
|
26290
|
-
if (process.env.EXPRESS_DEBUG) {
|
|
26291
|
-
console.log(` [DEBUG] ETS KDC start response: ${kdcStartText.slice(0, 500)}`);
|
|
26292
|
-
}
|
|
26325
|
+
if (process.env.EXPRESS_DEBUG) log(` [DEBUG] ETS KDC start response: ${kdcStartText.slice(0, 500)}`);
|
|
26293
26326
|
try {
|
|
26294
26327
|
const kdcStartData = JSON.parse(kdcStartText);
|
|
26295
26328
|
const keyBody = kdcStartData.result ?? kdcStartText;
|
|
26296
26329
|
serverPublicKey = new Uint8Array(Buffer.from(keyBody, "base64"));
|
|
26297
26330
|
serverPublicKeyId = "kdc-start-ets";
|
|
26298
26331
|
const rawB64 = Buffer.from(serverPublicKey).toString("base64");
|
|
26299
|
-
|
|
26332
|
+
log(` ETS server public key from /kdc/start: ${rawB64} (curve25519, used directly)`);
|
|
26300
26333
|
} catch {
|
|
26301
26334
|
try {
|
|
26302
26335
|
serverPublicKey = new Uint8Array(Buffer.from(kdcStartText, "base64"));
|
|
@@ -26306,11 +26339,7 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26306
26339
|
}
|
|
26307
26340
|
if (!serverPublicKey.length) {
|
|
26308
26341
|
if (process.env.EXPRESS_DEBUG && !etsKdcStartRes.ok) {
|
|
26309
|
-
|
|
26310
|
-
try {
|
|
26311
|
-
console.log(` [DEBUG] ETS KDC start body: ${(await etsKdcStartRes.text()).slice(0, 500)}`);
|
|
26312
|
-
} catch {
|
|
26313
|
-
}
|
|
26342
|
+
log(` [DEBUG] ETS KDC start status: ${etsKdcStartRes.status}`);
|
|
26314
26343
|
}
|
|
26315
26344
|
throw new Error("Could not fetch server public key from ETS /kdc/start");
|
|
26316
26345
|
}
|
|
@@ -26320,34 +26349,26 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26320
26349
|
const [ctsRtsRes, etsRtsRes] = await Promise.all([
|
|
26321
26350
|
fetch(kdcSignUrl, {
|
|
26322
26351
|
method: "POST",
|
|
26323
|
-
headers: {
|
|
26324
|
-
...commonHeaders(webOrigin),
|
|
26325
|
-
Authorization: `Bearer ${accessToken}`,
|
|
26326
|
-
"Content-Type": "application/json"
|
|
26327
|
-
},
|
|
26352
|
+
headers: { ...commonHeaders(webOrigin), Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
26328
26353
|
body: rtsKeyBody
|
|
26329
26354
|
}),
|
|
26330
26355
|
fetch(`${etsBaseUrl}/api/v2/kdc/keys/${userHuid}`, {
|
|
26331
26356
|
method: "POST",
|
|
26332
|
-
headers: {
|
|
26333
|
-
...commonHeaders(webOrigin),
|
|
26334
|
-
Authorization: `Bearer ${etsAuthToken}`,
|
|
26335
|
-
"Content-Type": "application/json"
|
|
26336
|
-
},
|
|
26357
|
+
headers: { ...commonHeaders(webOrigin), Authorization: `Bearer ${etsAuthToken}`, "Content-Type": "application/json" },
|
|
26337
26358
|
body: rtsKeyBody
|
|
26338
26359
|
})
|
|
26339
26360
|
]);
|
|
26340
26361
|
if (!ctsRtsRes.ok) {
|
|
26341
26362
|
const errText = await ctsRtsRes.text().catch(() => "");
|
|
26342
|
-
|
|
26363
|
+
log(` Warning: CTS RTS key registration failed (${ctsRtsRes.status}): ${errText.slice(0, 200)}`);
|
|
26343
26364
|
} else {
|
|
26344
|
-
|
|
26365
|
+
log(` RTS key registered in CTS: ${rtsPublicKeyId}`);
|
|
26345
26366
|
}
|
|
26346
26367
|
if (!etsRtsRes.ok) {
|
|
26347
26368
|
const errText = await etsRtsRes.text().catch(() => "");
|
|
26348
|
-
|
|
26369
|
+
log(` Warning: ETS RTS key registration failed (${etsRtsRes.status}): ${errText.slice(0, 200)}`);
|
|
26349
26370
|
} else {
|
|
26350
|
-
|
|
26371
|
+
log(` RTS key registered in ETS: ${rtsPublicKeyId}`);
|
|
26351
26372
|
}
|
|
26352
26373
|
}
|
|
26353
26374
|
if (!rtsPrivateKey) {
|
|
@@ -26359,34 +26380,26 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26359
26380
|
const [ctsEncRes, etsEncRes] = await Promise.all([
|
|
26360
26381
|
fetch(kdcSignUrl, {
|
|
26361
26382
|
method: "POST",
|
|
26362
|
-
headers: {
|
|
26363
|
-
...commonHeaders(webOrigin),
|
|
26364
|
-
Authorization: `Bearer ${accessToken}`,
|
|
26365
|
-
"Content-Type": "application/json"
|
|
26366
|
-
},
|
|
26383
|
+
headers: { ...commonHeaders(webOrigin), Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
26367
26384
|
body: rtsFallbackBody
|
|
26368
26385
|
}),
|
|
26369
26386
|
fetch(`${etsBaseUrl}/api/v2/kdc/keys/${userHuid}`, {
|
|
26370
26387
|
method: "POST",
|
|
26371
|
-
headers: {
|
|
26372
|
-
...commonHeaders(webOrigin),
|
|
26373
|
-
Authorization: `Bearer ${etsAuthToken}`,
|
|
26374
|
-
"Content-Type": "application/json"
|
|
26375
|
-
},
|
|
26388
|
+
headers: { ...commonHeaders(webOrigin), Authorization: `Bearer ${etsAuthToken}`, "Content-Type": "application/json" },
|
|
26376
26389
|
body: rtsFallbackBody
|
|
26377
26390
|
})
|
|
26378
26391
|
]);
|
|
26379
26392
|
if (!ctsEncRes.ok) {
|
|
26380
26393
|
const errText = await ctsEncRes.text().catch(() => "");
|
|
26381
|
-
|
|
26394
|
+
log(` Warning: CTS fallback RTS key registration failed (${ctsEncRes.status}): ${errText.slice(0, 200)}`);
|
|
26382
26395
|
} else {
|
|
26383
|
-
|
|
26396
|
+
log(` Fallback encryption key registered in CTS: ${rtsPublicKeyId}`);
|
|
26384
26397
|
}
|
|
26385
26398
|
if (!etsEncRes.ok) {
|
|
26386
26399
|
const errText = await etsEncRes.text().catch(() => "");
|
|
26387
|
-
|
|
26400
|
+
log(` Warning: ETS fallback RTS key registration failed (${etsEncRes.status}): ${errText.slice(0, 200)}`);
|
|
26388
26401
|
} else {
|
|
26389
|
-
|
|
26402
|
+
log(` Fallback encryption key registered in ETS: ${rtsPublicKeyId}`);
|
|
26390
26403
|
}
|
|
26391
26404
|
}
|
|
26392
26405
|
const rtsPublicKey = nacl3.box.keyPair.fromSecretKey(rtsPrivateKey).publicKey;
|
|
@@ -26395,18 +26408,14 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26395
26408
|
try {
|
|
26396
26409
|
rtsAuthToken = decryptRtsToken(encryptedRtsToken, serverPublicKey, rtsPrivateKey);
|
|
26397
26410
|
setRtsAuthToken(rtsAuthToken);
|
|
26398
|
-
if (process.env.EXPRESS_DEBUG) {
|
|
26399
|
-
|
|
26400
|
-
}
|
|
26401
|
-
console.log(` RTS auth token decrypted from encrypted_rts_token`);
|
|
26411
|
+
if (process.env.EXPRESS_DEBUG) log(` [DEBUG] Decrypted RTS auth token: ${rtsAuthToken.slice(0, 60)}...`);
|
|
26412
|
+
log(` RTS auth token decrypted from encrypted_rts_token`);
|
|
26402
26413
|
} catch (err) {
|
|
26403
|
-
|
|
26414
|
+
log(` Warning: could not decrypt encrypted_rts_token: ${err.message}`);
|
|
26404
26415
|
}
|
|
26405
26416
|
}
|
|
26406
26417
|
const rtsIdFromToken = extractRtsKeyIdFromToken(accessToken);
|
|
26407
|
-
if (rtsIdFromToken && process.env.EXPRESS_DEBUG) {
|
|
26408
|
-
console.log(` [DEBUG] rts_id from CTS token: ${rtsIdFromToken}`);
|
|
26409
|
-
}
|
|
26418
|
+
if (rtsIdFromToken && process.env.EXPRESS_DEBUG) log(` [DEBUG] rts_id from CTS token: ${rtsIdFromToken}`);
|
|
26410
26419
|
const existingCts = loadApigwKeys()?.ctsKey;
|
|
26411
26420
|
let ctsKey;
|
|
26412
26421
|
if (qrCtsPrivateKey && qrCtsKeyId) {
|
|
@@ -26415,10 +26424,10 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26415
26424
|
privateKey: qrCtsPrivateKey,
|
|
26416
26425
|
publicKey: nacl3.box.keyPair.fromSecretKey(qrCtsPrivateKey).publicKey
|
|
26417
26426
|
};
|
|
26418
|
-
|
|
26427
|
+
log(` Using CTS key from QR handshake: ${qrCtsKeyId.slice(0, 8)}... (shared account key)`);
|
|
26419
26428
|
} else if (existingCts) {
|
|
26420
26429
|
ctsKey = existingCts;
|
|
26421
|
-
|
|
26430
|
+
log(` Reusing existing CTS key: ${existingCts.keyId.slice(0, 8)}... (not re-registering)`);
|
|
26422
26431
|
} else {
|
|
26423
26432
|
const currentCts = await fetchCurrentAccountCtsKey2(getBaseUrl(config), accessToken, userHuid, webOrigin);
|
|
26424
26433
|
if (currentCts) {
|
|
@@ -26426,29 +26435,25 @@ async function qrLogin(cliOverrides = {}) {
|
|
|
26426
26435
|
`Account already has a shared CTS key (${currentCts}) that this CLI doesn't hold.
|
|
26427
26436
|
Minting a new one would break your other devices (they can't fetch its private key).
|
|
26428
26437
|
Instead, extract the key from a logged-in web client (IndexedDB authState \u2192 encryptionKeys \u2192 user.privateKeys.cts) and run:
|
|
26429
|
-
express auth import-cts <private_key_b64> ${currentCts}
|
|
26438
|
+
express-cli auth import-cts <private_key_b64> ${currentCts}
|
|
26430
26439
|
Then re-run login, or just use 'auth refresh' for tokens.`
|
|
26431
26440
|
);
|
|
26432
26441
|
}
|
|
26433
|
-
|
|
26442
|
+
log(" No existing account CTS key found \u2014 minting a new one (first device).");
|
|
26434
26443
|
const ctsKeyPair = nacl3.box.keyPair();
|
|
26435
26444
|
const ctsKeyId = crypto.randomUUID();
|
|
26436
26445
|
const ctsKeyPubB64 = Buffer.from(ctsKeyPair.publicKey).toString("base64");
|
|
26437
26446
|
const ctsKeyBody = JSON.stringify({ key: ctsKeyPubB64, kind: "cts", algo: "xsalsa20", id: ctsKeyId });
|
|
26438
26447
|
const ctsCtsKeyRes = await fetch(kdcSignUrl, {
|
|
26439
26448
|
method: "POST",
|
|
26440
|
-
headers: {
|
|
26441
|
-
...commonHeaders(webOrigin),
|
|
26442
|
-
Authorization: `Bearer ${accessToken}`,
|
|
26443
|
-
"Content-Type": "application/json"
|
|
26444
|
-
},
|
|
26449
|
+
headers: { ...commonHeaders(webOrigin), Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
26445
26450
|
body: ctsKeyBody
|
|
26446
26451
|
});
|
|
26447
26452
|
if (!ctsCtsKeyRes.ok) {
|
|
26448
26453
|
const errText = await ctsCtsKeyRes.text().catch(() => "");
|
|
26449
|
-
|
|
26454
|
+
log(` Warning: CTS encryption key registration failed (${ctsCtsKeyRes.status}): ${errText.slice(0, 200)}`);
|
|
26450
26455
|
} else {
|
|
26451
|
-
|
|
26456
|
+
log(` CTS encryption key registered: ${ctsKeyId.slice(0, 8)}...`);
|
|
26452
26457
|
}
|
|
26453
26458
|
ctsKey = {
|
|
26454
26459
|
keyId: ctsKeyId,
|
|
@@ -26468,7 +26473,7 @@ Then re-run login, or just use 'auth refresh' for tokens.`
|
|
|
26468
26473
|
serverPublicKeyId
|
|
26469
26474
|
};
|
|
26470
26475
|
saveApigwKeys(apigwKeys);
|
|
26471
|
-
|
|
26476
|
+
log("\nStep 6: Activating apigw via ETS...");
|
|
26472
26477
|
const activationUrl = `${etsBaseUrl}/api/v1/apigw/api/v1/authentication/activation`;
|
|
26473
26478
|
const activationBody = JSON.stringify({
|
|
26474
26479
|
app_version: config.app_version,
|
|
@@ -26496,20 +26501,39 @@ Then re-run login, or just use 'auth refresh' for tokens.`
|
|
|
26496
26501
|
});
|
|
26497
26502
|
if (!activationRes.ok) {
|
|
26498
26503
|
const actErrText = await activationRes.text().catch(() => "");
|
|
26499
|
-
|
|
26504
|
+
log(` Warning: apigw activation failed (${activationRes.status}): ${actErrText.slice(0, 200)}`);
|
|
26500
26505
|
} else {
|
|
26501
|
-
|
|
26506
|
+
log(" Apigw activated.");
|
|
26502
26507
|
}
|
|
26503
|
-
|
|
26508
|
+
log(`
|
|
26504
26509
|
User HUID: ${userHuid || "unknown"}`);
|
|
26505
|
-
|
|
26506
|
-
|
|
26507
|
-
|
|
26508
|
-
|
|
26510
|
+
log(` Signing key: ${apigwSigningKey.keyId.slice(0, 8)}...`);
|
|
26511
|
+
log(` Encryption key: ${rtsPublicKeyId.slice(0, 8)}...`);
|
|
26512
|
+
log(` Server key: ${serverPublicKeyId.slice(0, 8)}...`);
|
|
26513
|
+
log("\nQR login complete! You are now authenticated.");
|
|
26514
|
+
}
|
|
26515
|
+
async function qrLogin(cliOverrides = {}) {
|
|
26516
|
+
const mat = buildQrMaterial(cliOverrides);
|
|
26517
|
+
console.log("Step 1/6: Scan this QR code with your eXpress app:\n");
|
|
26518
|
+
qrcode.generate(mat.qrPayload, { small: true }, (qr) => {
|
|
26519
|
+
console.log(qr);
|
|
26520
|
+
});
|
|
26521
|
+
console.log(`
|
|
26522
|
+
registration_id: ${mat.registrationId}`);
|
|
26523
|
+
console.log(" Waiting for scan (server long-polling)...\n");
|
|
26524
|
+
openQrInBrowser(mat.qrPayload, mat.registrationId).catch(() => {
|
|
26525
|
+
});
|
|
26526
|
+
const pollResult = await pollForQrScan(mat);
|
|
26527
|
+
await completeQrRegistration(mat, pollResult);
|
|
26509
26528
|
}
|
|
26510
26529
|
|
|
26530
|
+
// src/cli/auth.ts
|
|
26531
|
+
init_store();
|
|
26532
|
+
|
|
26511
26533
|
// src/auth/token-refresh.ts
|
|
26512
26534
|
init_esm_shims();
|
|
26535
|
+
init_store();
|
|
26536
|
+
init_loader();
|
|
26513
26537
|
var refreshPromise = null;
|
|
26514
26538
|
async function refreshToken(cliOverrides = {}) {
|
|
26515
26539
|
if (refreshPromise) return refreshPromise;
|
|
@@ -26657,6 +26681,7 @@ import { Command as Command2 } from "commander";
|
|
|
26657
26681
|
|
|
26658
26682
|
// src/api/client.ts
|
|
26659
26683
|
init_esm_shims();
|
|
26684
|
+
init_store();
|
|
26660
26685
|
var ApiClient = class {
|
|
26661
26686
|
config;
|
|
26662
26687
|
baseUrl;
|
|
@@ -27126,8 +27151,97 @@ async function fetchChatListViaWebSocket(params) {
|
|
|
27126
27151
|
});
|
|
27127
27152
|
});
|
|
27128
27153
|
}
|
|
27154
|
+
async function createPersonalChatViaWebSocket(params) {
|
|
27155
|
+
const { host, ctsToken, encryptionKeyId, myHuid, targetHuid, timeoutMs = 15e3 } = params;
|
|
27156
|
+
const instanceId = randomUUID3();
|
|
27157
|
+
const wsUrl = `wss://${host}/socket/user/websocket?vsn=1.0.0&auto_join=true&key_id=${encryptionKeyId}&version=6&background=false&instance_id=${instanceId}`;
|
|
27158
|
+
return new Promise((resolve, reject) => {
|
|
27159
|
+
let settled = false;
|
|
27160
|
+
const done = (fn) => {
|
|
27161
|
+
if (!settled) {
|
|
27162
|
+
settled = true;
|
|
27163
|
+
fn();
|
|
27164
|
+
}
|
|
27165
|
+
};
|
|
27166
|
+
const timer = setTimeout(() => {
|
|
27167
|
+
try {
|
|
27168
|
+
ws2.close();
|
|
27169
|
+
} catch {
|
|
27170
|
+
}
|
|
27171
|
+
done(() => reject(new Error("WebSocket timeout: no chat_new response")));
|
|
27172
|
+
}, timeoutMs);
|
|
27173
|
+
const hostParts = host.split(".");
|
|
27174
|
+
const webOrigin = `https://${hostParts.length > 2 ? hostParts.slice(1).join(".") : host}`;
|
|
27175
|
+
const ws2 = new WebSocket(wsUrl, {
|
|
27176
|
+
headers: {
|
|
27177
|
+
Origin: webOrigin,
|
|
27178
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
|
27179
|
+
}
|
|
27180
|
+
});
|
|
27181
|
+
const authRef = 0;
|
|
27182
|
+
const createRef = 1;
|
|
27183
|
+
const send = (msg) => {
|
|
27184
|
+
ws2.send(JSON.stringify(msg));
|
|
27185
|
+
};
|
|
27186
|
+
ws2.addEventListener("open", () => {
|
|
27187
|
+
send({ topic: "phoenix", event: "authenticate", payload: { token: ctsToken }, ref: authRef });
|
|
27188
|
+
});
|
|
27189
|
+
ws2.addEventListener("message", (event) => {
|
|
27190
|
+
let msg;
|
|
27191
|
+
try {
|
|
27192
|
+
msg = JSON.parse(event.data);
|
|
27193
|
+
} catch {
|
|
27194
|
+
return;
|
|
27195
|
+
}
|
|
27196
|
+
if (msg.ref === authRef && msg.event === "phx_reply") {
|
|
27197
|
+
if (msg.payload.status !== "ok") {
|
|
27198
|
+
clearTimeout(timer);
|
|
27199
|
+
ws2.close();
|
|
27200
|
+
done(() => reject(new Error(`WS auth failed: ${JSON.stringify(msg.payload.response)}`)));
|
|
27201
|
+
return;
|
|
27202
|
+
}
|
|
27203
|
+
send({
|
|
27204
|
+
topic: "system",
|
|
27205
|
+
event: "chat_new",
|
|
27206
|
+
payload: {
|
|
27207
|
+
chat_type: "chat",
|
|
27208
|
+
members: [targetHuid, myHuid],
|
|
27209
|
+
name: "personal chat",
|
|
27210
|
+
description: "",
|
|
27211
|
+
threads_enabled: false
|
|
27212
|
+
},
|
|
27213
|
+
ref: createRef
|
|
27214
|
+
});
|
|
27215
|
+
return;
|
|
27216
|
+
}
|
|
27217
|
+
if (msg.ref === createRef && msg.event === "phx_reply") {
|
|
27218
|
+
clearTimeout(timer);
|
|
27219
|
+
ws2.close();
|
|
27220
|
+
if (msg.payload.status !== "ok") {
|
|
27221
|
+
done(() => reject(new Error(`chat_new failed: ${JSON.stringify(msg.payload.response)}`)));
|
|
27222
|
+
return;
|
|
27223
|
+
}
|
|
27224
|
+
const chatId = msg.payload.response?.chat_new?.payload?.group_chat_id;
|
|
27225
|
+
if (!chatId) {
|
|
27226
|
+
done(() => reject(new Error(`chat_new: no group_chat_id in response`)));
|
|
27227
|
+
return;
|
|
27228
|
+
}
|
|
27229
|
+
done(() => resolve(chatId));
|
|
27230
|
+
}
|
|
27231
|
+
});
|
|
27232
|
+
ws2.addEventListener("error", (err) => {
|
|
27233
|
+
clearTimeout(timer);
|
|
27234
|
+
done(() => reject(new Error(`WS error: ${String(err)}`)));
|
|
27235
|
+
});
|
|
27236
|
+
ws2.addEventListener("close", (event) => {
|
|
27237
|
+
clearTimeout(timer);
|
|
27238
|
+
done(() => reject(new Error(`WS closed before chat_new reply (code=${event.code})`)));
|
|
27239
|
+
});
|
|
27240
|
+
});
|
|
27241
|
+
}
|
|
27129
27242
|
|
|
27130
27243
|
// src/api/chats.ts
|
|
27244
|
+
init_store();
|
|
27131
27245
|
var ChatsApi = class {
|
|
27132
27246
|
constructor(client) {
|
|
27133
27247
|
this.client = client;
|
|
@@ -27174,6 +27288,16 @@ var ChatsApi = class {
|
|
|
27174
27288
|
});
|
|
27175
27289
|
return data?.open_chats?.[0] ?? null;
|
|
27176
27290
|
}
|
|
27291
|
+
async createDm(targetHuid) {
|
|
27292
|
+
const config = loadConfig();
|
|
27293
|
+
const host = new URL(getBaseUrl(config)).hostname;
|
|
27294
|
+
const ctsToken = getAuthToken();
|
|
27295
|
+
const keys = loadApigwKeys();
|
|
27296
|
+
const encryptionKeyId = (keys?.ctsKey ?? keys?.encryptionKey)?.keyId;
|
|
27297
|
+
if (!ctsToken || !encryptionKeyId) throw new Error("Not authenticated");
|
|
27298
|
+
const myHuid = (await new UserApi(this.client).getSelfProfile()).user_huid;
|
|
27299
|
+
return createPersonalChatViaWebSocket({ host, ctsToken, encryptionKeyId, myHuid, targetHuid });
|
|
27300
|
+
}
|
|
27177
27301
|
};
|
|
27178
27302
|
|
|
27179
27303
|
// src/api/phonebook.ts
|
|
@@ -27221,8 +27345,7 @@ var PhonebookApi = class {
|
|
|
27221
27345
|
);
|
|
27222
27346
|
if (!data || typeof data !== "object") return [];
|
|
27223
27347
|
const obj = data;
|
|
27224
|
-
const
|
|
27225
|
-
const phonebook = result?.phonebook ?? [];
|
|
27348
|
+
const phonebook = obj.phonebook ?? [];
|
|
27226
27349
|
const huids = [
|
|
27227
27350
|
...new Set(
|
|
27228
27351
|
phonebook.flatMap((entry) => entry.contacts ?? []).map((c) => c.user_huid).filter(Boolean)
|
|
@@ -27646,6 +27769,7 @@ import WebSocket2 from "ws";
|
|
|
27646
27769
|
import { randomBytes as randomBytes3, randomUUID as randomUUID4 } from "crypto";
|
|
27647
27770
|
import nacl4 from "tweetnacl";
|
|
27648
27771
|
import sodium from "libsodium-wrappers-sumo";
|
|
27772
|
+
init_store();
|
|
27649
27773
|
function buildTextPayload(text, fromHuid, chatId) {
|
|
27650
27774
|
return JSON.stringify({
|
|
27651
27775
|
type: "text",
|
|
@@ -27875,14 +27999,28 @@ async function resolveChatId(client, chatIdOrName) {
|
|
|
27875
27999
|
const chats = await listChatsWithNames(client);
|
|
27876
28000
|
const lower = chatIdOrName.toLowerCase();
|
|
27877
28001
|
const matches = chats.filter((c) => (c.name ?? "").toLowerCase().includes(lower));
|
|
27878
|
-
if (matches.length ===
|
|
28002
|
+
if (matches.length === 1) return matches[0].group_chat_id;
|
|
27879
28003
|
if (matches.length > 1) {
|
|
27880
28004
|
const names = matches.map((c) => ` ${c.name} (${c.group_chat_id})`).join("\n");
|
|
27881
28005
|
throw new Error(`Multiple chats match "${chatIdOrName}":
|
|
27882
28006
|
${names}
|
|
27883
28007
|
Use the full chat ID.`);
|
|
27884
28008
|
}
|
|
27885
|
-
|
|
28009
|
+
const results = await new PhonebookApi(client).searchUsers(chatIdOrName, 5);
|
|
28010
|
+
if (results.length === 0) throw new Error(`No chat or contact found matching "${chatIdOrName}"`);
|
|
28011
|
+
if (results.length > 1) {
|
|
28012
|
+
const names = results.map((p) => ` ${p.name} (${p.user_huid})`).join("\n");
|
|
28013
|
+
throw new Error(`No existing DM with "${chatIdOrName}", found multiple contacts:
|
|
28014
|
+
${names}
|
|
28015
|
+
Be more specific.`);
|
|
28016
|
+
}
|
|
28017
|
+
const person = results[0];
|
|
28018
|
+
process.stderr.write(`No DM with "${person.name}" \u2014 creating one...
|
|
28019
|
+
`);
|
|
28020
|
+
const chatId = await new ChatsApi(client).createDm(person.user_huid);
|
|
28021
|
+
process.stderr.write(`DM created: ${chatId}
|
|
28022
|
+
`);
|
|
28023
|
+
return chatId;
|
|
27886
28024
|
}
|
|
27887
28025
|
|
|
27888
28026
|
// src/cli/send.ts
|
|
@@ -28048,6 +28186,7 @@ import { Command as Command11 } from "commander";
|
|
|
28048
28186
|
init_esm_shims();
|
|
28049
28187
|
import WebSocket3 from "ws";
|
|
28050
28188
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
28189
|
+
init_store();
|
|
28051
28190
|
|
|
28052
28191
|
// src/api/decrypt.ts
|
|
28053
28192
|
init_esm_shims();
|
|
@@ -28249,6 +28388,7 @@ init_esm_shims();
|
|
|
28249
28388
|
import WebSocket4 from "ws";
|
|
28250
28389
|
import { EventEmitter } from "events";
|
|
28251
28390
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
28391
|
+
init_store();
|
|
28252
28392
|
var HEARTBEAT_MS = 25e3;
|
|
28253
28393
|
var REQUEST_TIMEOUT_MS = 15e3;
|
|
28254
28394
|
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
@@ -38447,9 +38587,23 @@ init_esm_shims();
|
|
|
38447
38587
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
38448
38588
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
38449
38589
|
import { z as z2 } from "zod";
|
|
38590
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync } from "fs";
|
|
38591
|
+
import { tmpdir } from "os";
|
|
38592
|
+
import { join } from "path";
|
|
38593
|
+
init_store();
|
|
38594
|
+
import qrcode2 from "qrcode-terminal";
|
|
38595
|
+
var AUTH_INSTRUCTION = "Run 'express-cli auth qr' (or 'npx @ih8e/express-cli auth qr') in a terminal to log in.";
|
|
38450
38596
|
var ok = (data) => ({
|
|
38451
38597
|
content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }]
|
|
38452
38598
|
});
|
|
38599
|
+
var notAuthenticated = () => ok({ error: "not_authenticated", instruction: AUTH_INSTRUCTION });
|
|
38600
|
+
async function ensureAuth() {
|
|
38601
|
+
const exp = getTokenExpiresAt();
|
|
38602
|
+
if (exp && Date.now() > exp - 5 * 60 * 1e3) {
|
|
38603
|
+
await refreshToken();
|
|
38604
|
+
}
|
|
38605
|
+
return !!getAuthToken();
|
|
38606
|
+
}
|
|
38453
38607
|
var Inbox = class {
|
|
38454
38608
|
buf = [];
|
|
38455
38609
|
seq = 0;
|
|
@@ -38535,6 +38689,7 @@ async function runMcpServer() {
|
|
|
38535
38689
|
description: "List chats (DMs, groups, channels) with names and full chat IDs. DM names are resolved to the person's full name.",
|
|
38536
38690
|
inputSchema: { type: z2.enum(["all", "dm", "group", "channel"]).optional().describe("Filter by chat type (default all)") }
|
|
38537
38691
|
}, async ({ type }) => {
|
|
38692
|
+
if (!await ensureAuth()) return notAuthenticated();
|
|
38538
38693
|
const chats = await listChatsWithNames(new ApiClient());
|
|
38539
38694
|
const kind = { dm: "chat", group: "group_chat", channel: "channel" };
|
|
38540
38695
|
const filtered = !type || type === "all" ? chats : chats.filter((c) => c.chat_type === kind[type]);
|
|
@@ -38545,6 +38700,7 @@ async function runMcpServer() {
|
|
|
38545
38700
|
description: "Find chats whose name (person or group) contains the query. Returns name + full chat_id to use with other tools.",
|
|
38546
38701
|
inputSchema: { query: z2.string().describe("Part of the chat or person name") }
|
|
38547
38702
|
}, async ({ query }) => {
|
|
38703
|
+
if (!await ensureAuth()) return notAuthenticated();
|
|
38548
38704
|
const chats = await listChatsWithNames(new ApiClient());
|
|
38549
38705
|
const q = query.toLowerCase();
|
|
38550
38706
|
return ok(chats.filter((c) => (c.name ?? "").toLowerCase().includes(q)).map((c) => ({ name: c.name, chat_id: c.group_chat_id, type: c.chat_type })));
|
|
@@ -38557,6 +38713,7 @@ async function runMcpServer() {
|
|
|
38557
38713
|
limit: z2.number().int().min(1).max(200).optional().describe("How many recent messages (default 20)")
|
|
38558
38714
|
}
|
|
38559
38715
|
}, async ({ chat, limit }) => {
|
|
38716
|
+
if (!await ensureAuth()) return notAuthenticated();
|
|
38560
38717
|
const client = new ApiClient();
|
|
38561
38718
|
const chatId = await resolveChatId(client, chat);
|
|
38562
38719
|
const msgs = await readMessages({ chatId, limit: limit ?? 20 });
|
|
@@ -38568,9 +38725,10 @@ async function runMcpServer() {
|
|
|
38568
38725
|
});
|
|
38569
38726
|
server.registerTool("send_message", {
|
|
38570
38727
|
title: "Send a message",
|
|
38571
|
-
description: "Send a text message to a chat, identified by name (partial ok) or chat_id. Returns the sync_id.",
|
|
38728
|
+
description: "Send a text message to a chat, identified by name (partial ok) or chat_id. If no DM exists with that person, one is created automatically. Returns the sync_id.",
|
|
38572
38729
|
inputSchema: { chat: z2.string().describe("Chat name or full chat_id"), text: z2.string().describe("Message text") }
|
|
38573
38730
|
}, async ({ chat, text }) => {
|
|
38731
|
+
if (!await ensureAuth()) return notAuthenticated();
|
|
38574
38732
|
const client = new ApiClient();
|
|
38575
38733
|
const chatId = await resolveChatId(client, chat);
|
|
38576
38734
|
const res = await sendMessageViaWebSocket({ client, chatId, body: text });
|
|
@@ -38581,6 +38739,7 @@ async function runMcpServer() {
|
|
|
38581
38739
|
description: "Global company phonebook search across all employees by name.",
|
|
38582
38740
|
inputSchema: { query: z2.string().describe("Name to search"), limit: z2.number().int().min(1).max(50).optional() }
|
|
38583
38741
|
}, async ({ query, limit }) => {
|
|
38742
|
+
if (!await ensureAuth()) return notAuthenticated();
|
|
38584
38743
|
const profiles = await new PhonebookApi(new ApiClient()).searchUsers(query, limit ?? 20);
|
|
38585
38744
|
return ok(profiles.map((p) => ({ name: p.name, huid: p.user_huid, email: p.email, position: p.company_position, department: p.department })));
|
|
38586
38745
|
});
|
|
@@ -38588,26 +38747,123 @@ async function runMcpServer() {
|
|
|
38588
38747
|
title: "My profile",
|
|
38589
38748
|
description: "Get the authenticated user's own profile.",
|
|
38590
38749
|
inputSchema: {}
|
|
38591
|
-
}, async () =>
|
|
38750
|
+
}, async () => {
|
|
38751
|
+
if (!await ensureAuth()) return notAuthenticated();
|
|
38752
|
+
return ok(await new UserApi(new ApiClient()).getSelfProfile());
|
|
38753
|
+
});
|
|
38592
38754
|
server.registerTool("wait_for_messages", {
|
|
38593
38755
|
title: "Wait for incoming messages",
|
|
38594
38756
|
description: "Block until new incoming messages arrive (from any chat/discussion), then return them. Returns messages received since the previous call to this tool; if none are pending, waits up to timeout_seconds. Your own sent messages are not included. Use this to react to new messages instead of polling.",
|
|
38595
38757
|
inputSchema: { timeout_seconds: z2.number().int().min(1).max(120).optional().describe("Max seconds to wait when nothing is pending (default 30)") }
|
|
38596
38758
|
}, async ({ timeout_seconds }) => {
|
|
38597
|
-
if (!session) return ok({ error: "Session unavailable
|
|
38759
|
+
if (!session) return ok({ error: "Session unavailable.", instruction: AUTH_INSTRUCTION });
|
|
38598
38760
|
const items = await inbox.take((timeout_seconds ?? 30) * 1e3);
|
|
38599
38761
|
return ok({ connected: sessionReady, count: items.length, messages: await enrich(items) });
|
|
38600
38762
|
});
|
|
38601
38763
|
server.registerTool("status", {
|
|
38602
38764
|
title: "Auth status",
|
|
38603
|
-
description: "Check authentication and access-token status.",
|
|
38765
|
+
description: "Check authentication and access-token status. Call this first if other tools return not_authenticated.",
|
|
38604
38766
|
inputSchema: {}
|
|
38605
38767
|
}, async () => {
|
|
38606
38768
|
const exp = getTokenExpiresAt();
|
|
38607
38769
|
return ok({
|
|
38608
38770
|
authenticated: !!getAuthToken(),
|
|
38609
|
-
token_expires_in_seconds: exp ? Math.max(0, Math.floor((exp - Date.now()) / 1e3)) : null
|
|
38771
|
+
token_expires_in_seconds: exp ? Math.max(0, Math.floor((exp - Date.now()) / 1e3)) : null,
|
|
38772
|
+
login_instruction: AUTH_INSTRUCTION
|
|
38773
|
+
});
|
|
38774
|
+
});
|
|
38775
|
+
const QR_MATERIAL_FILE = join(tmpdir(), "express-qr-material.json");
|
|
38776
|
+
let pendingQrMaterial = null;
|
|
38777
|
+
let pendingQrPoll = null;
|
|
38778
|
+
server.registerTool("auth_qr_start", {
|
|
38779
|
+
title: "Start QR login \u2014 step 1 of 2",
|
|
38780
|
+
description: "Step 1: generate a QR code for eXpress login. The QR code is included in the response \u2014 display it to the user as-is. After displaying the QR you MUST immediately call the MCP tool `auth_qr_poll` (step 2) \u2014 do NOT wait for the user to confirm scanning first; auth_qr_poll waits automatically. Do NOT run auth_qr_poll as a shell command \u2014 it is an MCP tool. Use when status reports not_authenticated.",
|
|
38781
|
+
inputSchema: {}
|
|
38782
|
+
}, async () => {
|
|
38783
|
+
const material = buildQrMaterial();
|
|
38784
|
+
let qrAscii = "";
|
|
38785
|
+
qrcode2.generate(material.qrPayload, { small: true }, (qr) => {
|
|
38786
|
+
qrAscii = qr;
|
|
38787
|
+
});
|
|
38788
|
+
writeFileSync3(join(tmpdir(), "express-qr.txt"), qrAscii, "utf8");
|
|
38789
|
+
process.stderr.write("\n" + qrAscii + "\n");
|
|
38790
|
+
openQrInBrowser(material.qrPayload, material.registrationId).catch(() => {
|
|
38610
38791
|
});
|
|
38792
|
+
const persistent = {
|
|
38793
|
+
registrationId: material.registrationId,
|
|
38794
|
+
encryptionKey: Buffer.from(material.encryptionKey).toString("base64"),
|
|
38795
|
+
qrSigningKey: {
|
|
38796
|
+
keyId: material.qrSigningKey.keyId,
|
|
38797
|
+
privateKey: Buffer.from(material.qrSigningKey.privateKey).toString("base64"),
|
|
38798
|
+
publicKey: Buffer.from(material.qrSigningKey.publicKey).toString("base64")
|
|
38799
|
+
},
|
|
38800
|
+
udid: material.udid,
|
|
38801
|
+
qrPayload: material.qrPayload,
|
|
38802
|
+
qrBody: material.qrBody
|
|
38803
|
+
};
|
|
38804
|
+
writeFileSync3(QR_MATERIAL_FILE, JSON.stringify(persistent), "utf8");
|
|
38805
|
+
pendingQrMaterial = material;
|
|
38806
|
+
pendingQrPoll = pollForQrScan(material).catch((err) => {
|
|
38807
|
+
process.stderr.write(`[mcp] QR poll error: ${err.message}
|
|
38808
|
+
`);
|
|
38809
|
+
pendingQrMaterial = null;
|
|
38810
|
+
pendingQrPoll = null;
|
|
38811
|
+
throw err;
|
|
38812
|
+
});
|
|
38813
|
+
return ok(
|
|
38814
|
+
`${qrAscii.trim()}
|
|
38815
|
+
|
|
38816
|
+
Scan with the eXpress mobile app.
|
|
38817
|
+
|
|
38818
|
+
[AGENT: call auth_qr_poll immediately \u2014 do not wait for user input]`
|
|
38819
|
+
);
|
|
38820
|
+
});
|
|
38821
|
+
server.registerTool("auth_qr_poll", {
|
|
38822
|
+
title: "Complete QR login \u2014 step 2 of 2",
|
|
38823
|
+
description: "Step 2: wait for the phone scan and complete QR login. Call this immediately after auth_qr_start \u2014 do NOT wait for user confirmation. Waits up to 90 s for the server handshake, then saves tokens and keys. IMPORTANT: call this as an MCP tool, not as a shell command.",
|
|
38824
|
+
inputSchema: {}
|
|
38825
|
+
}, async () => {
|
|
38826
|
+
let material = pendingQrMaterial;
|
|
38827
|
+
let poll = pendingQrPoll;
|
|
38828
|
+
if (!material || !poll) {
|
|
38829
|
+
let saved = null;
|
|
38830
|
+
try {
|
|
38831
|
+
saved = JSON.parse(readFileSync3(QR_MATERIAL_FILE, "utf8"));
|
|
38832
|
+
} catch {
|
|
38833
|
+
}
|
|
38834
|
+
if (!saved) return ok("No pending QR session. Call auth_qr_start first.");
|
|
38835
|
+
const sk = saved.qrSigningKey;
|
|
38836
|
+
const hydrated = {
|
|
38837
|
+
registrationId: saved.registrationId,
|
|
38838
|
+
encryptionKey: Buffer.from(saved.encryptionKey, "base64"),
|
|
38839
|
+
qrSigningKey: {
|
|
38840
|
+
keyId: sk.keyId,
|
|
38841
|
+
privateKey: Buffer.from(sk.privateKey, "base64"),
|
|
38842
|
+
publicKey: Buffer.from(sk.publicKey, "base64")
|
|
38843
|
+
},
|
|
38844
|
+
udid: saved.udid,
|
|
38845
|
+
qrPayload: saved.qrPayload,
|
|
38846
|
+
qrBody: saved.qrBody,
|
|
38847
|
+
config: (await Promise.resolve().then(() => (init_loader(), loader_exports))).loadConfig()
|
|
38848
|
+
};
|
|
38849
|
+
material = hydrated;
|
|
38850
|
+
poll = pollForQrScan(material);
|
|
38851
|
+
}
|
|
38852
|
+
pendingQrMaterial = null;
|
|
38853
|
+
pendingQrPoll = null;
|
|
38854
|
+
try {
|
|
38855
|
+
unlinkSync(QR_MATERIAL_FILE);
|
|
38856
|
+
} catch {
|
|
38857
|
+
}
|
|
38858
|
+
try {
|
|
38859
|
+
const pollResult = await poll;
|
|
38860
|
+
await completeQrRegistration(material, pollResult, (msg) => {
|
|
38861
|
+
process.stderr.write(msg + "\n");
|
|
38862
|
+
});
|
|
38863
|
+
return ok("Login successful. You are now authenticated.");
|
|
38864
|
+
} catch (err) {
|
|
38865
|
+
return ok(`Login failed: ${err.message}`);
|
|
38866
|
+
}
|
|
38611
38867
|
});
|
|
38612
38868
|
await server.connect(new StdioServerTransport());
|
|
38613
38869
|
}
|
|
@@ -38629,7 +38885,7 @@ function createMcpCommand() {
|
|
|
38629
38885
|
// src/cli/root.ts
|
|
38630
38886
|
function createRootCommand() {
|
|
38631
38887
|
const program2 = new Command15();
|
|
38632
|
-
program2.name("express-cli").description("CLI client for eXpress Chat").version("0.1.
|
|
38888
|
+
program2.name("express-cli").description("CLI client for eXpress Chat").version("0.1.5");
|
|
38633
38889
|
program2.addCommand(createAuthCommand());
|
|
38634
38890
|
program2.addCommand(createApiCommand());
|
|
38635
38891
|
program2.addCommand(createConfigCommand());
|