neozip-cli 0.95.0 ā 0.95.1
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/CHANGELOG.md +12 -0
- package/README.md +148 -44
- package/dist/src/account/token-service-funding.js +151 -0
- package/dist/src/account/token-service-identity.js +488 -0
- package/dist/src/commands/verifyEmail.js +146 -0
- package/dist/src/config/ConfigStore.js +373 -0
- package/package.json +2 -2
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IdentityKeyConflictError = exports.WalletLoginUnavailableError = void 0;
|
|
4
|
+
exports.registerEmailWithDelivery = registerEmailWithDelivery;
|
|
5
|
+
exports.registerEmailApp = registerEmailApp;
|
|
6
|
+
exports.registerEmailCli = registerEmailCli;
|
|
7
|
+
exports.verifyEmailAndExtractToken = verifyEmailAndExtractToken;
|
|
8
|
+
exports.exchangeMagicLinkToken = exchangeMagicLinkToken;
|
|
9
|
+
exports.requestWalletLoginChallenge = requestWalletLoginChallenge;
|
|
10
|
+
exports.walletLogin = walletLogin;
|
|
11
|
+
exports.requestPhoneOtp = requestPhoneOtp;
|
|
12
|
+
exports.verifyPhoneOtp = verifyPhoneOtp;
|
|
13
|
+
exports.fetchCoordinatorConfig = fetchCoordinatorConfig;
|
|
14
|
+
exports.requestWalletEnsureChallenge = requestWalletEnsureChallenge;
|
|
15
|
+
exports.completeWalletEnsure = completeWalletEnsure;
|
|
16
|
+
exports.requestWalletAttachChallenge = requestWalletAttachChallenge;
|
|
17
|
+
exports.completeWalletAttach = completeWalletAttach;
|
|
18
|
+
exports.runWalletEnsureAndAttach = runWalletEnsureAndAttach;
|
|
19
|
+
exports.getIdentityKeyBundle = getIdentityKeyBundle;
|
|
20
|
+
exports.requestIdentityKeyInitChallenge = requestIdentityKeyInitChallenge;
|
|
21
|
+
exports.completeIdentityKeyInit = completeIdentityKeyInit;
|
|
22
|
+
const ethers_1 = require("ethers");
|
|
23
|
+
const recipient_lookup_1 = require("../archive/recipient-lookup");
|
|
24
|
+
const token_service_fetch_1 = require("../util/token-service-fetch");
|
|
25
|
+
const token_auth_1 = require("../connection/token-auth");
|
|
26
|
+
const wallet_evm_1 = require("./wallet-evm");
|
|
27
|
+
const DEFAULT_RECIPIENT_SUITE = "ecies-x25519-aes256gcm";
|
|
28
|
+
function challengeBody(bundle) {
|
|
29
|
+
return {
|
|
30
|
+
x25519PublicKeyB64: bundle.x25519PublicKeyB64,
|
|
31
|
+
wrappedPrivateKeyB64: bundle.wrappedPrivateKeyB64,
|
|
32
|
+
wrapFormatVersion: bundle.wrapFormatVersion,
|
|
33
|
+
wrapKdfInfo: bundle.wrapKdfInfo,
|
|
34
|
+
wrapKdfSaltB64: bundle.wrapKdfSaltB64,
|
|
35
|
+
wrapAead: bundle.wrapAead,
|
|
36
|
+
wrapIvB64: bundle.wrapIvB64,
|
|
37
|
+
wrapAadB64: bundle.wrapAadB64,
|
|
38
|
+
recipientSuites: bundle.recipientSuites ?? [DEFAULT_RECIPIENT_SUITE],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function pickString(obj, ...keys) {
|
|
42
|
+
for (const k of keys) {
|
|
43
|
+
const v = obj[k];
|
|
44
|
+
if (typeof v === "string" && v.length > 0)
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function extractError(parsed, fallback, options) {
|
|
50
|
+
const authHint = options?.status != null
|
|
51
|
+
? (0, token_auth_1.tokenServiceAuthErrorMessage)(options.status, options.email)
|
|
52
|
+
: null;
|
|
53
|
+
if (authHint)
|
|
54
|
+
return authHint;
|
|
55
|
+
if (parsed && typeof parsed === "object") {
|
|
56
|
+
const p = parsed;
|
|
57
|
+
if (typeof p.error === "string")
|
|
58
|
+
return p.error;
|
|
59
|
+
if (typeof p.message === "string")
|
|
60
|
+
return p.message;
|
|
61
|
+
}
|
|
62
|
+
return fallback;
|
|
63
|
+
}
|
|
64
|
+
async function safeJson(response) {
|
|
65
|
+
const ct = response.headers.get("content-type") || "";
|
|
66
|
+
if (!ct.includes("json"))
|
|
67
|
+
return null;
|
|
68
|
+
try {
|
|
69
|
+
return (await response.json());
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function registerEmailWithDelivery(baseUrl, email, verificationDelivery) {
|
|
76
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
77
|
+
let res;
|
|
78
|
+
try {
|
|
79
|
+
res = await fetch(`${url}/auth/register`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "content-type": "application/json" },
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
email: email.trim().toLowerCase(),
|
|
84
|
+
verificationDelivery,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return { success: false, error: (0, token_service_fetch_1.formatTokenServiceFetchError)(url, err) };
|
|
90
|
+
}
|
|
91
|
+
const parsed = await safeJson(res);
|
|
92
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: extractError(parsed, `HTTP ${res.status}`),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
message: typeof parsed.message === "string" ? parsed.message : "Verification email sent.",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Desktop-style registration (deep link email). */
|
|
104
|
+
async function registerEmailApp(baseUrl, email) {
|
|
105
|
+
return registerEmailWithDelivery(baseUrl, email, "app");
|
|
106
|
+
}
|
|
107
|
+
/** Terminal / neozip connect registration (6-digit code only). */
|
|
108
|
+
async function registerEmailCli(baseUrl, email) {
|
|
109
|
+
return registerEmailWithDelivery(baseUrl, email, "cli");
|
|
110
|
+
}
|
|
111
|
+
async function verifyEmailAndExtractToken(baseUrl, email, code) {
|
|
112
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
113
|
+
let res;
|
|
114
|
+
try {
|
|
115
|
+
res = await fetch(`${url}/auth/verify-email-code`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "content-type": "application/json" },
|
|
118
|
+
body: JSON.stringify({ email: email.trim().toLowerCase(), code: code.trim() }),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
throw new Error((0, token_service_fetch_1.formatTokenServiceFetchError)(url, err));
|
|
123
|
+
}
|
|
124
|
+
const parsed = await safeJson(res);
|
|
125
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
126
|
+
throw new Error(extractError(parsed, `Verification failed (HTTP ${res.status})`, {
|
|
127
|
+
status: res.status,
|
|
128
|
+
email,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
const accessToken = pickString(parsed, "accessToken", "token", "bearerToken") ?? null;
|
|
132
|
+
if (!accessToken) {
|
|
133
|
+
throw new Error("Verification succeeded but no accessToken in response. Set NEOZIP_TOKEN_SERVICE_ACCESS_TOKEN manually or update neozip-blockchain.");
|
|
134
|
+
}
|
|
135
|
+
const verifiedEmail = pickString(parsed, "email") ?? email.trim().toLowerCase();
|
|
136
|
+
return {
|
|
137
|
+
accessToken,
|
|
138
|
+
email: verifiedEmail,
|
|
139
|
+
expiresAt: (0, token_auth_1.parseAccessTokenExpiry)(parsed),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function exchangeMagicLinkToken(baseUrl, token) {
|
|
143
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
144
|
+
let res;
|
|
145
|
+
try {
|
|
146
|
+
res = await fetch(`${url}/auth/exchange-token`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "content-type": "application/json" },
|
|
149
|
+
body: JSON.stringify({ token: token.trim() }),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
throw new Error((0, token_service_fetch_1.formatTokenServiceFetchError)(url, err));
|
|
154
|
+
}
|
|
155
|
+
const parsed = await safeJson(res);
|
|
156
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
157
|
+
throw new Error(extractError(parsed, `Magic link exchange failed (HTTP ${res.status})`, {
|
|
158
|
+
status: res.status,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
const accessToken = pickString(parsed, "accessToken", "token", "bearerToken") ?? null;
|
|
162
|
+
if (!accessToken) {
|
|
163
|
+
throw new Error("Magic link exchange succeeded but no accessToken in response.");
|
|
164
|
+
}
|
|
165
|
+
const verifiedEmail = pickString(parsed, "email") ?? "";
|
|
166
|
+
if (!verifiedEmail) {
|
|
167
|
+
throw new Error("Magic link exchange succeeded but no email in response.");
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
accessToken,
|
|
171
|
+
email: verifiedEmail,
|
|
172
|
+
expiresAt: (0, token_auth_1.parseAccessTokenExpiry)(parsed),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Thrown when the Token Service does not expose the wallet-login endpoints (older deployment). */
|
|
176
|
+
class WalletLoginUnavailableError extends Error {
|
|
177
|
+
constructor(message = 'Token Service does not support wallet login') {
|
|
178
|
+
super(message);
|
|
179
|
+
this.name = 'WalletLoginUnavailableError';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.WalletLoginUnavailableError = WalletLoginUnavailableError;
|
|
183
|
+
async function requestWalletLoginChallenge(baseUrl, evmAddress) {
|
|
184
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
185
|
+
const checksummed = (0, ethers_1.getAddress)(evmAddress.trim());
|
|
186
|
+
let res;
|
|
187
|
+
try {
|
|
188
|
+
res = await fetch(`${url}/auth/wallet-login/challenge`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'content-type': 'application/json' },
|
|
191
|
+
body: JSON.stringify({ evmAddress: checksummed }),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
throw new Error((0, token_service_fetch_1.formatTokenServiceFetchError)(url, err));
|
|
196
|
+
}
|
|
197
|
+
if (res.status === 404) {
|
|
198
|
+
throw new WalletLoginUnavailableError();
|
|
199
|
+
}
|
|
200
|
+
const parsed = await safeJson(res);
|
|
201
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
202
|
+
throw new Error(extractError(parsed, `Wallet login challenge failed (HTTP ${res.status})`));
|
|
203
|
+
}
|
|
204
|
+
const challengeId = pickString(parsed, 'challengeId');
|
|
205
|
+
const message = pickString(parsed, 'message');
|
|
206
|
+
const addr = pickString(parsed, 'evmAddress');
|
|
207
|
+
if (!challengeId || !message || !addr) {
|
|
208
|
+
throw new Error('Wallet login challenge response missing fields');
|
|
209
|
+
}
|
|
210
|
+
return { challengeId, message, evmAddress: addr };
|
|
211
|
+
}
|
|
212
|
+
async function walletLogin(baseUrl, input) {
|
|
213
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
214
|
+
let res;
|
|
215
|
+
try {
|
|
216
|
+
res = await fetch(`${url}/auth/wallet-login`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: { 'content-type': 'application/json' },
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
evmAddress: (0, ethers_1.getAddress)(input.evmAddress.trim()),
|
|
221
|
+
challengeId: input.challengeId,
|
|
222
|
+
evmSignature: input.evmSignature,
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
throw new Error((0, token_service_fetch_1.formatTokenServiceFetchError)(url, err));
|
|
228
|
+
}
|
|
229
|
+
if (res.status === 404) {
|
|
230
|
+
throw new WalletLoginUnavailableError();
|
|
231
|
+
}
|
|
232
|
+
const parsed = await safeJson(res);
|
|
233
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
234
|
+
throw new Error(extractError(parsed, `Wallet login failed (HTTP ${res.status})`, { status: res.status }));
|
|
235
|
+
}
|
|
236
|
+
const accessToken = pickString(parsed, 'accessToken', 'token', 'bearerToken');
|
|
237
|
+
if (!accessToken) {
|
|
238
|
+
throw new Error('Wallet login succeeded but no accessToken in response.');
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
accessToken,
|
|
242
|
+
email: pickString(parsed, 'email'),
|
|
243
|
+
expiresAt: (0, token_auth_1.parseAccessTokenExpiry)(parsed),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async function requestPhoneOtp(baseUrl, accessToken, phoneE164) {
|
|
247
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
248
|
+
let res;
|
|
249
|
+
try {
|
|
250
|
+
res = await fetch(`${url}/auth/phone/request-otp`, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers: {
|
|
253
|
+
Authorization: `Bearer ${accessToken}`,
|
|
254
|
+
"content-type": "application/json",
|
|
255
|
+
},
|
|
256
|
+
body: JSON.stringify({ phoneE164: phoneE164.trim() }),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return { success: false, error: (0, token_service_fetch_1.formatTokenServiceFetchError)(url, err) };
|
|
261
|
+
}
|
|
262
|
+
const parsed = await safeJson(res);
|
|
263
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
error: extractError(parsed, `Phone OTP request failed (HTTP ${res.status})`),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { success: true };
|
|
270
|
+
}
|
|
271
|
+
async function verifyPhoneOtp(baseUrl, accessToken, phoneE164, code) {
|
|
272
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
273
|
+
let res;
|
|
274
|
+
try {
|
|
275
|
+
res = await fetch(`${url}/auth/phone/verify`, {
|
|
276
|
+
method: "POST",
|
|
277
|
+
headers: {
|
|
278
|
+
Authorization: `Bearer ${accessToken}`,
|
|
279
|
+
"content-type": "application/json",
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
phoneE164: phoneE164.trim(),
|
|
283
|
+
code: code.trim(),
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
return { success: false, error: (0, token_service_fetch_1.formatTokenServiceFetchError)(url, err) };
|
|
289
|
+
}
|
|
290
|
+
const parsed = await safeJson(res);
|
|
291
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
292
|
+
return {
|
|
293
|
+
success: false,
|
|
294
|
+
error: extractError(parsed, `Phone verification failed (HTTP ${res.status})`),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return { success: true };
|
|
298
|
+
}
|
|
299
|
+
class IdentityKeyConflictError extends Error {
|
|
300
|
+
constructor(message) {
|
|
301
|
+
super(message);
|
|
302
|
+
this.name = "IdentityKeyConflictError";
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
exports.IdentityKeyConflictError = IdentityKeyConflictError;
|
|
306
|
+
function authHeaders(accessToken) {
|
|
307
|
+
return {
|
|
308
|
+
Authorization: `Bearer ${accessToken}`,
|
|
309
|
+
"content-type": "application/json",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async function fetchCoordinatorConfig(baseUrl) {
|
|
313
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
314
|
+
try {
|
|
315
|
+
const res = await fetch(`${url}/crypto/coordinator-config`, { method: "GET" });
|
|
316
|
+
return res.ok;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function requestWalletEnsureChallenge(baseUrl, evmAddress) {
|
|
323
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
324
|
+
const checksummed = (0, ethers_1.getAddress)(evmAddress.trim());
|
|
325
|
+
const res = await fetch(`${url}/identity/wallets/challenge`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: { "content-type": "application/json" },
|
|
328
|
+
body: JSON.stringify({ evmAddress: checksummed }),
|
|
329
|
+
});
|
|
330
|
+
const parsed = await safeJson(res);
|
|
331
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
332
|
+
throw new Error(extractError(parsed, `Wallet challenge failed (HTTP ${res.status})`));
|
|
333
|
+
}
|
|
334
|
+
const challengeId = pickString(parsed, "challengeId");
|
|
335
|
+
const message = pickString(parsed, "message");
|
|
336
|
+
const addr = pickString(parsed, "evmAddress");
|
|
337
|
+
if (!challengeId || !message || !addr) {
|
|
338
|
+
throw new Error("Wallet challenge response missing fields");
|
|
339
|
+
}
|
|
340
|
+
return { challengeId, message, evmAddress: addr };
|
|
341
|
+
}
|
|
342
|
+
async function completeWalletEnsure(baseUrl, input) {
|
|
343
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
344
|
+
const checksummed = (0, ethers_1.getAddress)(input.evmAddress.trim());
|
|
345
|
+
const res = await fetch(`${url}/identity/wallets/ensure`, {
|
|
346
|
+
method: "POST",
|
|
347
|
+
headers: { "content-type": "application/json" },
|
|
348
|
+
body: JSON.stringify({
|
|
349
|
+
evmAddress: checksummed,
|
|
350
|
+
challengeId: input.challengeId,
|
|
351
|
+
evmSignature: input.evmSignature,
|
|
352
|
+
}),
|
|
353
|
+
});
|
|
354
|
+
const parsed = await safeJson(res);
|
|
355
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
356
|
+
throw new Error(extractError(parsed, `Wallet ensure failed (HTTP ${res.status})`));
|
|
357
|
+
}
|
|
358
|
+
const walletId = typeof parsed.walletId === "number" ? parsed.walletId : Number(parsed.walletId);
|
|
359
|
+
const addr = pickString(parsed, "evmAddress");
|
|
360
|
+
if (!Number.isFinite(walletId) || !addr) {
|
|
361
|
+
throw new Error("Wallet ensure response missing walletId");
|
|
362
|
+
}
|
|
363
|
+
return { walletId, evmAddress: addr };
|
|
364
|
+
}
|
|
365
|
+
async function requestWalletAttachChallenge(baseUrl, accessToken, walletId) {
|
|
366
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
367
|
+
const res = await fetch(`${url}/identity/users/me/wallets/${walletId}/attach/challenge`, { method: "POST", headers: authHeaders(accessToken) });
|
|
368
|
+
const parsed = await safeJson(res);
|
|
369
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
370
|
+
throw new Error(extractError(parsed, `Attach challenge failed (HTTP ${res.status})`));
|
|
371
|
+
}
|
|
372
|
+
const challengeId = pickString(parsed, "challengeId");
|
|
373
|
+
const message = pickString(parsed, "message");
|
|
374
|
+
const wid = typeof parsed.walletId === "number" ? parsed.walletId : Number(parsed.walletId);
|
|
375
|
+
const addr = pickString(parsed, "evmAddress");
|
|
376
|
+
if (!challengeId || !message || !Number.isFinite(wid) || !addr) {
|
|
377
|
+
throw new Error("Attach challenge response missing fields");
|
|
378
|
+
}
|
|
379
|
+
return { challengeId, message, walletId: wid, evmAddress: addr };
|
|
380
|
+
}
|
|
381
|
+
async function completeWalletAttach(baseUrl, accessToken, walletId, input) {
|
|
382
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
383
|
+
const res = await fetch(`${url}/identity/users/me/wallets/${walletId}/attach/complete`, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: authHeaders(accessToken),
|
|
386
|
+
body: JSON.stringify({
|
|
387
|
+
challengeId: input.challengeId,
|
|
388
|
+
evmSignature: input.evmSignature,
|
|
389
|
+
setPrimary: true,
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
const parsed = await safeJson(res);
|
|
393
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
394
|
+
throw new Error(extractError(parsed, `Attach complete failed (HTTP ${res.status})`));
|
|
395
|
+
}
|
|
396
|
+
const linkId = typeof parsed.linkId === "number" ? parsed.linkId : Number(parsed.linkId);
|
|
397
|
+
if (!Number.isFinite(linkId)) {
|
|
398
|
+
throw new Error("Attach complete response missing linkId");
|
|
399
|
+
}
|
|
400
|
+
return { linkId };
|
|
401
|
+
}
|
|
402
|
+
async function runWalletEnsureAndAttach(baseUrl, accessToken, evmPrivateKey, evmAddress) {
|
|
403
|
+
const ensureCh = await requestWalletEnsureChallenge(baseUrl, evmAddress);
|
|
404
|
+
const ensureSig = await (0, wallet_evm_1.signEvmMessage)(evmPrivateKey, ensureCh.message);
|
|
405
|
+
const ensured = await completeWalletEnsure(baseUrl, {
|
|
406
|
+
evmAddress,
|
|
407
|
+
challengeId: ensureCh.challengeId,
|
|
408
|
+
evmSignature: ensureSig,
|
|
409
|
+
});
|
|
410
|
+
const attachCh = await requestWalletAttachChallenge(baseUrl, accessToken, ensured.walletId);
|
|
411
|
+
const attachSig = await (0, wallet_evm_1.signEvmMessage)(evmPrivateKey, attachCh.message);
|
|
412
|
+
const { linkId } = await completeWalletAttach(baseUrl, accessToken, ensured.walletId, {
|
|
413
|
+
challengeId: attachCh.challengeId,
|
|
414
|
+
evmSignature: attachSig,
|
|
415
|
+
});
|
|
416
|
+
return { walletId: ensured.walletId, evmAddress: ensured.evmAddress, linkId };
|
|
417
|
+
}
|
|
418
|
+
async function getIdentityKeyBundle(baseUrl, accessToken, walletId) {
|
|
419
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
420
|
+
const res = await fetch(`${url}/identity/wallets/${walletId}/identity-key`, {
|
|
421
|
+
method: "GET",
|
|
422
|
+
headers: authHeaders(accessToken),
|
|
423
|
+
});
|
|
424
|
+
if (res.status === 404)
|
|
425
|
+
return { found: false };
|
|
426
|
+
const parsed = await safeJson(res);
|
|
427
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
428
|
+
throw new Error(extractError(parsed, `identity-key GET failed (HTTP ${res.status})`));
|
|
429
|
+
}
|
|
430
|
+
const ik = parsed.identityKey;
|
|
431
|
+
if (!ik)
|
|
432
|
+
throw new Error("identity-key response missing identityKey");
|
|
433
|
+
const identityKeyId = typeof ik.identityKeyId === "number" ? ik.identityKeyId : Number(ik.identityKeyId);
|
|
434
|
+
const x25519PublicKey = pickString(ik, "x25519PublicKeyB64", "x25519PublicKey");
|
|
435
|
+
if (!Number.isFinite(identityKeyId) || !x25519PublicKey) {
|
|
436
|
+
throw new Error("identity-key response incomplete");
|
|
437
|
+
}
|
|
438
|
+
return { found: true, identityKeyId, x25519PublicKey };
|
|
439
|
+
}
|
|
440
|
+
async function requestIdentityKeyInitChallenge(baseUrl, accessToken, walletId, bundle) {
|
|
441
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
442
|
+
const res = await fetch(`${url}/identity/wallets/${walletId}/identity-key/init/challenge`, {
|
|
443
|
+
method: "POST",
|
|
444
|
+
headers: authHeaders(accessToken),
|
|
445
|
+
body: JSON.stringify(challengeBody(bundle)),
|
|
446
|
+
});
|
|
447
|
+
const parsed = await safeJson(res);
|
|
448
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
449
|
+
throw new Error(extractError(parsed, `init challenge failed (HTTP ${res.status})`));
|
|
450
|
+
}
|
|
451
|
+
const challengeId = pickString(parsed, "challengeId");
|
|
452
|
+
const message = pickString(parsed, "message");
|
|
453
|
+
const evmAddress = pickString(parsed, "evmAddress");
|
|
454
|
+
const wrappedSha256Hex = pickString(parsed, "wrappedSha256Hex");
|
|
455
|
+
if (!challengeId || !message || !evmAddress || !wrappedSha256Hex) {
|
|
456
|
+
throw new Error("init challenge response missing fields");
|
|
457
|
+
}
|
|
458
|
+
return { challengeId, message, evmAddress, wrappedSha256Hex };
|
|
459
|
+
}
|
|
460
|
+
async function completeIdentityKeyInit(baseUrl, accessToken, walletId, bundle, challengeId, evmSignature) {
|
|
461
|
+
const url = (0, recipient_lookup_1.resolveTokenServiceBaseUrl)({ tokenServiceUrl: baseUrl });
|
|
462
|
+
const res = await fetch(`${url}/identity/wallets/${walletId}/identity-key/init/complete`, {
|
|
463
|
+
method: "POST",
|
|
464
|
+
headers: authHeaders(accessToken),
|
|
465
|
+
body: JSON.stringify({
|
|
466
|
+
...challengeBody(bundle),
|
|
467
|
+
challengeId,
|
|
468
|
+
evmSignature,
|
|
469
|
+
}),
|
|
470
|
+
});
|
|
471
|
+
const parsed = await safeJson(res);
|
|
472
|
+
if (res.status === 409) {
|
|
473
|
+
throw new IdentityKeyConflictError(extractError(parsed, "Identity key already exists for this wallet."));
|
|
474
|
+
}
|
|
475
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
476
|
+
throw new Error(extractError(parsed, `init complete failed (HTTP ${res.status})`));
|
|
477
|
+
}
|
|
478
|
+
const ik = parsed.identityKey;
|
|
479
|
+
if (!ik)
|
|
480
|
+
throw new Error("init complete missing identityKey");
|
|
481
|
+
const identityKeyId = typeof ik.identityKeyId === "number" ? ik.identityKeyId : Number(ik.identityKeyId);
|
|
482
|
+
const x25519PublicKey = pickString(ik, "x25519PublicKeyB64", "x25519PublicKey");
|
|
483
|
+
if (!Number.isFinite(identityKeyId) || !x25519PublicKey) {
|
|
484
|
+
throw new Error("init complete response incomplete");
|
|
485
|
+
}
|
|
486
|
+
return { identityKeyId, x25519PublicKey };
|
|
487
|
+
}
|
|
488
|
+
//# sourceMappingURL=token-service-identity.js.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Verify email for Token Service timestamping
|
|
4
|
+
*
|
|
5
|
+
* Sets a default email for Token Service timestamps. Checks with the server whether
|
|
6
|
+
* the email is already verified; if not, sends a magic link. Always saves as default.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.runVerifyEmail = runVerifyEmail;
|
|
43
|
+
const readline = __importStar(require("readline"));
|
|
44
|
+
const token_service_1 = require("neozip-blockchain/token-service");
|
|
45
|
+
const ConfigStore_1 = require("../config/ConfigStore");
|
|
46
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
47
|
+
function isValidEmail(email) {
|
|
48
|
+
return EMAIL_REGEX.test(email.trim());
|
|
49
|
+
}
|
|
50
|
+
function isAlreadyVerifiedMessage(msg) {
|
|
51
|
+
if (!msg)
|
|
52
|
+
return false;
|
|
53
|
+
const lower = msg.toLowerCase();
|
|
54
|
+
return ((lower.includes('already') && (lower.includes('verified') || lower.includes('registered'))) ||
|
|
55
|
+
lower.includes('already verified') ||
|
|
56
|
+
lower.includes('email already'));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run the verify-email flow.
|
|
60
|
+
* Calls the server to check verification status; if already verified, sets as default.
|
|
61
|
+
* If not, sends magic link and sets as default. Always saves email to config.
|
|
62
|
+
*/
|
|
63
|
+
async function runVerifyEmail(options) {
|
|
64
|
+
const rl = readline.createInterface({
|
|
65
|
+
input: process.stdin,
|
|
66
|
+
output: process.stdout,
|
|
67
|
+
});
|
|
68
|
+
const prompt = (question, defaultValue) => {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const display = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
|
|
71
|
+
rl.question(display, (answer) => {
|
|
72
|
+
resolve((answer.trim() || defaultValue || '').trim());
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
console.log('\nš§ Token Service Email Configuration\n');
|
|
78
|
+
console.log('Token Service requires a verified email to create timestamped ZIP files.');
|
|
79
|
+
console.log('');
|
|
80
|
+
const config = ConfigStore_1.ConfigStore.getConfig();
|
|
81
|
+
const defaultEmail = options?.email || config.timestampEmail || '';
|
|
82
|
+
const email = await prompt('Email address', defaultEmail || undefined);
|
|
83
|
+
if (!email) {
|
|
84
|
+
console.error('ā Email is required');
|
|
85
|
+
rl.close();
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (!isValidEmail(email)) {
|
|
89
|
+
console.error('ā Invalid email format');
|
|
90
|
+
rl.close();
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// --set-default: skip server check, just save
|
|
94
|
+
if (options?.setDefaultOnly) {
|
|
95
|
+
const saved = ConfigStore_1.ConfigStore.set('tokenService.timestampEmail', email);
|
|
96
|
+
if (!saved) {
|
|
97
|
+
console.error('ā Failed to save email to configuration');
|
|
98
|
+
rl.close();
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
console.log(`\nā
${email} set as default for Token Service timestamps.`);
|
|
102
|
+
console.log('\nš” You can now create timestamped ZIPs: neozip -ts <archive> <files>\n');
|
|
103
|
+
rl.close();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
// Check with server: register returns status (already verified vs magic link sent)
|
|
107
|
+
console.log(`\nš¤ Checking verification status for ${email}...`);
|
|
108
|
+
const registerResult = await (0, token_service_1.registerEmail)(email, {
|
|
109
|
+
debug: options?.debug,
|
|
110
|
+
});
|
|
111
|
+
if (!registerResult.success) {
|
|
112
|
+
console.error('ā Failed to register email:', registerResult.error || 'Unknown error');
|
|
113
|
+
rl.close();
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const res = registerResult;
|
|
117
|
+
const msg = res.message || '';
|
|
118
|
+
const alreadyVerified = res.verified === true ||
|
|
119
|
+
res.alreadyVerified === true ||
|
|
120
|
+
isAlreadyVerifiedMessage(msg);
|
|
121
|
+
const saved = ConfigStore_1.ConfigStore.set('tokenService.timestampEmail', email);
|
|
122
|
+
if (!saved) {
|
|
123
|
+
console.error('ā Failed to save email to configuration');
|
|
124
|
+
rl.close();
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (alreadyVerified) {
|
|
128
|
+
console.log('\nā
Email is already verified. Set as default.');
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log('\nā
Verification link sent. Check your inbox (and spam folder).');
|
|
132
|
+
console.log(' Click the magic link in the email to complete verification.');
|
|
133
|
+
console.log(` ${email} has been set as default and will work once you've clicked the link.`);
|
|
134
|
+
}
|
|
135
|
+
console.log('\nš” You can now create timestamped ZIPs: neozip -ts <archive> <files>\n');
|
|
136
|
+
rl.close();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
141
|
+
console.error('ā Error:', msg);
|
|
142
|
+
rl.close();
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=verifyEmail.js.map
|