nebulon-escrow-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/bin/nebulon.js +3 -0
- package/idl/nebulon.json +4320 -0
- package/package.json +33 -0
- package/src/capsules.js +154 -0
- package/src/cli.js +292 -0
- package/src/commands/capsule.js +46 -0
- package/src/commands/config.js +445 -0
- package/src/commands/contract.js +4895 -0
- package/src/commands/hosted.js +90 -0
- package/src/commands/init.js +458 -0
- package/src/commands/invites.js +345 -0
- package/src/commands/login.js +320 -0
- package/src/commands/logout.js +22 -0
- package/src/commands/reset.js +102 -0
- package/src/commands/status.js +72 -0
- package/src/commands/tee.js +169 -0
- package/src/commands/wallet.js +166 -0
- package/src/config.js +80 -0
- package/src/constants.js +70 -0
- package/src/hosted.js +288 -0
- package/src/nebulon.js +121 -0
- package/src/paths.js +24 -0
- package/src/privacy.js +117 -0
- package/src/session.js +131 -0
- package/src/solana.js +196 -0
- package/src/ui.js +36 -0
- package/src/wallets.js +85 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
const { prompt } = require("enquirer");
|
|
2
|
+
const { loadConfig } = require("../config");
|
|
3
|
+
const { ensureHostedSession } = require("../session");
|
|
4
|
+
const {
|
|
5
|
+
getInvites,
|
|
6
|
+
acceptInviteById,
|
|
7
|
+
declineInvite,
|
|
8
|
+
setContractKey,
|
|
9
|
+
getContractKeys,
|
|
10
|
+
getContract,
|
|
11
|
+
} = require("../hosted");
|
|
12
|
+
const { ensureContractKeypair } = require("../privacy");
|
|
13
|
+
const { successMessage } = require("../ui");
|
|
14
|
+
|
|
15
|
+
const formatCreatedAt = (value) => {
|
|
16
|
+
if (!value) {
|
|
17
|
+
return "n/a";
|
|
18
|
+
}
|
|
19
|
+
const date = new Date(value * 1000);
|
|
20
|
+
if (Number.isNaN(date.getTime())) {
|
|
21
|
+
return "n/a";
|
|
22
|
+
}
|
|
23
|
+
return date.toISOString().replace("T", " ").slice(0, 19);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const formatHandle = (handle, wallet) => {
|
|
27
|
+
if (handle) {
|
|
28
|
+
return `@${handle}`;
|
|
29
|
+
}
|
|
30
|
+
if (wallet) {
|
|
31
|
+
return `${wallet.slice(0, 4)}...${wallet.slice(-4)}`;
|
|
32
|
+
}
|
|
33
|
+
return "n/a";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const resolveOptions = (options) => {
|
|
37
|
+
if (!options) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
if (typeof options.opts === "function") {
|
|
41
|
+
return options.opts();
|
|
42
|
+
}
|
|
43
|
+
return options;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const printInvites = (invites, options = {}) => {
|
|
47
|
+
const resolved = resolveOptions(options);
|
|
48
|
+
const label = resolved.showAll ? "Nebulon Invites (All)" : "Nebulon Invites";
|
|
49
|
+
console.log(label);
|
|
50
|
+
if (!invites.length) {
|
|
51
|
+
console.log("No invites found.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.log("# Type Status Role Contract ID From Date");
|
|
55
|
+
invites.forEach((invite, index) => {
|
|
56
|
+
const row = [
|
|
57
|
+
String(index + 1).padEnd(3, " "),
|
|
58
|
+
String(invite.type).padEnd(9, " "),
|
|
59
|
+
String(invite.status).padEnd(9, " "),
|
|
60
|
+
String(invite.role).padEnd(11, " "),
|
|
61
|
+
String(invite.contractId).padEnd(36, " "),
|
|
62
|
+
String(invite.counterparty || "n/a").padEnd(12, " "),
|
|
63
|
+
invite.createdAt,
|
|
64
|
+
];
|
|
65
|
+
console.log(row.join(" "));
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const runInviteList = async (filter, options = {}) => {
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
if (config.mode !== "hosted") {
|
|
72
|
+
console.error("Direct mode invites are not implemented yet.");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await ensureHostedSession(config, { quiet: true, requireHandle: true });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(error.message);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = await getInvites(config.backendUrl, config.auth.token);
|
|
83
|
+
const sent = Array.isArray(result.sent) ? result.sent : [];
|
|
84
|
+
const received = Array.isArray(result.received) ? result.received : [];
|
|
85
|
+
const resolved = resolveOptions(options);
|
|
86
|
+
let showAll = Boolean(resolved.showAll);
|
|
87
|
+
if (!showAll && Array.isArray(process.argv)) {
|
|
88
|
+
showAll = process.argv.includes("--show-all");
|
|
89
|
+
}
|
|
90
|
+
if (typeof filter === "string" && filter.trim().toLowerCase() === "--show-all") {
|
|
91
|
+
showAll = true;
|
|
92
|
+
filter = null;
|
|
93
|
+
}
|
|
94
|
+
const pendingOnly = !showAll;
|
|
95
|
+
const sentFiltered = pendingOnly
|
|
96
|
+
? sent.filter((invite) => invite.status === "pending")
|
|
97
|
+
: sent;
|
|
98
|
+
const receivedFiltered = pendingOnly
|
|
99
|
+
? received.filter((invite) => invite.status === "pending")
|
|
100
|
+
: received;
|
|
101
|
+
|
|
102
|
+
const sentRows = sentFiltered.map((invite) => ({
|
|
103
|
+
type: "sent",
|
|
104
|
+
status: invite.status,
|
|
105
|
+
role: invite.invitee_role || "n/a",
|
|
106
|
+
contractId: invite.contract_id,
|
|
107
|
+
counterparty: invite.invitee_handle
|
|
108
|
+
? `@${invite.invitee_handle}`
|
|
109
|
+
: invite.invitee_wallet
|
|
110
|
+
? formatHandle(null, invite.invitee_wallet)
|
|
111
|
+
: "link",
|
|
112
|
+
createdAt: formatCreatedAt(invite.created_at),
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const receivedRows = receivedFiltered.map((invite) => ({
|
|
116
|
+
type: "received",
|
|
117
|
+
status: invite.status,
|
|
118
|
+
role: invite.invitee_role || "n/a",
|
|
119
|
+
contractId: invite.contract_id,
|
|
120
|
+
counterparty: formatHandle(invite.issuer_handle, invite.issuer_wallet),
|
|
121
|
+
createdAt: formatCreatedAt(invite.created_at),
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
let rows = [...receivedRows, ...sentRows];
|
|
125
|
+
if (filter === "sent") {
|
|
126
|
+
rows = sentRows;
|
|
127
|
+
} else if (filter === "received") {
|
|
128
|
+
rows = receivedRows;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
printInvites(rows, options);
|
|
132
|
+
return { sent: sentFiltered, received: receivedFiltered, sentRows, receivedRows };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const resolveInviteSelection = (received, target) => {
|
|
136
|
+
const matchByIndex = Number.parseInt(target, 10);
|
|
137
|
+
if (!Number.isNaN(matchByIndex)) {
|
|
138
|
+
const idx = matchByIndex - 1;
|
|
139
|
+
if (idx >= 0 && idx < received.length) {
|
|
140
|
+
return received[idx];
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return received.find(
|
|
145
|
+
(invite) =>
|
|
146
|
+
invite.contract_id === target || invite.id === target
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const resolveInviteSelectionAll = (received, sent, target) => {
|
|
151
|
+
const combined = [...received, ...sent];
|
|
152
|
+
const matchByIndex = Number.parseInt(target, 10);
|
|
153
|
+
if (!Number.isNaN(matchByIndex)) {
|
|
154
|
+
const idx = matchByIndex - 1;
|
|
155
|
+
if (idx >= 0 && idx < combined.length) {
|
|
156
|
+
return combined[idx];
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return combined.find(
|
|
161
|
+
(invite) =>
|
|
162
|
+
invite.contract_id === target || invite.id === target
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const renderInviteDetails = (invite) => {
|
|
167
|
+
console.log("Invite details");
|
|
168
|
+
console.log(`Contract ID: ${invite.contract_id}`);
|
|
169
|
+
console.log(
|
|
170
|
+
`From: ${formatHandle(invite.issuer_handle, invite.issuer_wallet)}`
|
|
171
|
+
);
|
|
172
|
+
console.log(`Created: ${formatCreatedAt(invite.created_at)}`);
|
|
173
|
+
console.log("");
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const renderInviteStatus = async (config, invite) => {
|
|
177
|
+
const detail = await getContract(
|
|
178
|
+
config.backendUrl,
|
|
179
|
+
config.auth.token,
|
|
180
|
+
invite.contract_id
|
|
181
|
+
);
|
|
182
|
+
const contract = detail.contract;
|
|
183
|
+
if (!contract) {
|
|
184
|
+
console.error("Contract not found.");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const customer = formatHandle(contract.client_handle, contract.client_wallet);
|
|
188
|
+
const provider = formatHandle(
|
|
189
|
+
contract.contractor_handle,
|
|
190
|
+
contract.contractor_wallet
|
|
191
|
+
);
|
|
192
|
+
const selfWallet = config.auth.wallet;
|
|
193
|
+
const role =
|
|
194
|
+
contract.client_wallet === selfWallet
|
|
195
|
+
? "client"
|
|
196
|
+
: contract.contractor_wallet === selfWallet
|
|
197
|
+
? "contractor"
|
|
198
|
+
: "other";
|
|
199
|
+
console.log(`Invite ID: ${invite.id}`);
|
|
200
|
+
console.log(`Customer: ${customer}${contract.client_wallet === selfWallet ? " (YOU)" : ""}`);
|
|
201
|
+
console.log(
|
|
202
|
+
`Service Provider: ${provider}${contract.contractor_wallet === selfWallet ? " (YOU)" : ""}`
|
|
203
|
+
);
|
|
204
|
+
console.log(`Role: ${role}`);
|
|
205
|
+
console.log("");
|
|
206
|
+
console.log(`Current status: ${invite.status}`);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const runInviteAction = async (target, action) => {
|
|
210
|
+
const config = loadConfig();
|
|
211
|
+
if (config.mode !== "hosted") {
|
|
212
|
+
console.error("Direct mode invites are not implemented yet.");
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await ensureHostedSession(config, { quiet: true, requireHandle: true });
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(error.message);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!target) {
|
|
223
|
+
console.log("Usage: nebulon invite <index|contractId> [accept|deny]");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const result = await getInvites(config.backendUrl, config.auth.token);
|
|
228
|
+
const receivedAll = Array.isArray(result.received) ? result.received : [];
|
|
229
|
+
const sentAll = Array.isArray(result.sent) ? result.sent : [];
|
|
230
|
+
const receivedPending = receivedAll.filter((invite) => invite.status === "pending");
|
|
231
|
+
const actionKey = (action || "").toLowerCase();
|
|
232
|
+
const invite =
|
|
233
|
+
actionKey === "status"
|
|
234
|
+
? resolveInviteSelectionAll(receivedAll, sentAll, target.trim())
|
|
235
|
+
: resolveInviteSelection(receivedPending, target.trim());
|
|
236
|
+
if (!invite) {
|
|
237
|
+
console.error("Invite not found.");
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (actionKey === "status") {
|
|
242
|
+
await renderInviteStatus(config, invite);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
renderInviteDetails(invite);
|
|
247
|
+
|
|
248
|
+
const normalizeDecision = (value) => {
|
|
249
|
+
if (!value) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const lowered = value.trim().toLowerCase();
|
|
253
|
+
if (["accept", "a", "yes", "y"].includes(lowered)) {
|
|
254
|
+
return "accept";
|
|
255
|
+
}
|
|
256
|
+
if (["deny", "decline", "d", "no", "n"].includes(lowered)) {
|
|
257
|
+
return "deny";
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
let decision = normalizeDecision(action);
|
|
263
|
+
if (!decision) {
|
|
264
|
+
const answer = await prompt({
|
|
265
|
+
type: "input",
|
|
266
|
+
name: "decision",
|
|
267
|
+
message: "Do you want to accept this invite? (yes/no)",
|
|
268
|
+
initial: "yes",
|
|
269
|
+
validate: (value) =>
|
|
270
|
+
normalizeDecision(value) ? true : "Type yes or no.",
|
|
271
|
+
});
|
|
272
|
+
decision = normalizeDecision(answer.decision);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!decision) {
|
|
276
|
+
console.log("Usage: nebulon invite <index|contractId> [accept|deny]");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
if (decision === "accept") {
|
|
282
|
+
const accepted = await acceptInviteById(
|
|
283
|
+
config.backendUrl,
|
|
284
|
+
config.auth.token,
|
|
285
|
+
{ inviteId: invite.id }
|
|
286
|
+
);
|
|
287
|
+
console.log("Invite accepted.");
|
|
288
|
+
console.log(`Contract ID: ${accepted.contractId}`);
|
|
289
|
+
console.log(`Status: ${accepted.status}`);
|
|
290
|
+
// Fetch complete contract data to ensure we have escrow_pda
|
|
291
|
+
const contractDetail = await getContract(
|
|
292
|
+
config.backendUrl,
|
|
293
|
+
config.auth.token,
|
|
294
|
+
accepted.contractId
|
|
295
|
+
);
|
|
296
|
+
console.log("Setting up privacy keys...");
|
|
297
|
+
try {
|
|
298
|
+
const keypair = ensureContractKeypair(config, accepted.contractId);
|
|
299
|
+
await setContractKey(
|
|
300
|
+
config.backendUrl,
|
|
301
|
+
config.auth.token,
|
|
302
|
+
accepted.contractId,
|
|
303
|
+
keypair.publicKey
|
|
304
|
+
);
|
|
305
|
+
const keys = await getContractKeys(
|
|
306
|
+
config.backendUrl,
|
|
307
|
+
config.auth.token,
|
|
308
|
+
accepted.contractId
|
|
309
|
+
);
|
|
310
|
+
const count = Array.isArray(keys.keys) ? keys.keys.length : 0;
|
|
311
|
+
if (count > 1) {
|
|
312
|
+
console.log("Privacy key exchange complete.");
|
|
313
|
+
} else {
|
|
314
|
+
console.log("Privacy key registered. Awaiting counterparty.");
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.log("Privacy key setup pending. Try again later.");
|
|
318
|
+
}
|
|
319
|
+
successMessage("Invite accepted.");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const declined = await declineInvite(
|
|
324
|
+
config.backendUrl,
|
|
325
|
+
config.auth.token,
|
|
326
|
+
{ inviteId: invite.id }
|
|
327
|
+
);
|
|
328
|
+
console.log("Invite declined.");
|
|
329
|
+
console.log(`Contract ID: ${declined.contractId}`);
|
|
330
|
+
console.log(`Status: ${declined.status}`);
|
|
331
|
+
successMessage("Invite declined.");
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error && error.message === "Not Found") {
|
|
334
|
+
console.error("Invite action failed: backend missing invite action routes.");
|
|
335
|
+
console.error("Restart the backend after updating to the latest code.");
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
runInviteList,
|
|
344
|
+
runInviteAction,
|
|
345
|
+
};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const { prompt } = require("enquirer");
|
|
2
|
+
const chalk = require("chalk");
|
|
3
|
+
const { loadConfig, saveConfig } = require("../config");
|
|
4
|
+
const { successMessage } = require("../ui");
|
|
5
|
+
const { loadWalletKeypair } = require("../wallets");
|
|
6
|
+
const { getChallenge, signChallenge, verify, me, checkHandle, updateHandle } = require("../hosted");
|
|
7
|
+
|
|
8
|
+
const HANDLE_REGEX = /^[a-z0-9._-]+$/;
|
|
9
|
+
const PFP_CHOICES = [
|
|
10
|
+
{ name: "1 - galaxy", message: "1 - galaxy", value: "1" },
|
|
11
|
+
{ name: "2 - alien", message: "2 - alien", value: "2" },
|
|
12
|
+
{ name: "3 - star", message: "3 - star", value: "3" },
|
|
13
|
+
{ name: "4 - moon", message: "4 - moon", value: "4" },
|
|
14
|
+
{ name: "5 - spaceship", message: "5 - spaceship", value: "5" },
|
|
15
|
+
{ name: "6 - glyph", message: "6 - glyph", value: "6" },
|
|
16
|
+
{ name: "7 - saturn", message: "7 - saturn", value: "7" },
|
|
17
|
+
{ name: "8 - singularity", message: "8 - singularity", value: "8" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const isPromptCancel = (error) => {
|
|
21
|
+
if (!error) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (typeof error === "string") {
|
|
25
|
+
return error.trim() === "";
|
|
26
|
+
}
|
|
27
|
+
const message = (error.message || "").toLowerCase();
|
|
28
|
+
return (
|
|
29
|
+
message.includes("cancel") ||
|
|
30
|
+
message.includes("aborted") ||
|
|
31
|
+
message.includes("terminated")
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const nowSeconds = () => Math.floor(Date.now() / 1000);
|
|
36
|
+
|
|
37
|
+
const normalizeHandle = (value) =>
|
|
38
|
+
value.trim().toLowerCase().replace(/^@/, "");
|
|
39
|
+
|
|
40
|
+
const validateHandleLocally = (handle) => {
|
|
41
|
+
if (!handle) {
|
|
42
|
+
return { ok: false, reason: "missing" };
|
|
43
|
+
}
|
|
44
|
+
if (handle.length < 4 || handle.length > 16) {
|
|
45
|
+
return { ok: false, reason: "length" };
|
|
46
|
+
}
|
|
47
|
+
if (!HANDLE_REGEX.test(handle)) {
|
|
48
|
+
return { ok: false, reason: "characters" };
|
|
49
|
+
}
|
|
50
|
+
if (handle.startsWith(".") || handle.endsWith(".")) {
|
|
51
|
+
return { ok: false, reason: "dots" };
|
|
52
|
+
}
|
|
53
|
+
if (handle.includes("..")) {
|
|
54
|
+
return { ok: false, reason: "dots" };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, value: handle };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleReasonMessage = (reason) => {
|
|
60
|
+
switch (reason) {
|
|
61
|
+
case "missing":
|
|
62
|
+
return "ID is required.";
|
|
63
|
+
case "length":
|
|
64
|
+
return "Must be 4-16 characters.";
|
|
65
|
+
case "characters":
|
|
66
|
+
return "Only letters, numbers, dots, underscores, and hyphens.";
|
|
67
|
+
case "dots":
|
|
68
|
+
return "Cannot start/end with dots or have consecutive dots.";
|
|
69
|
+
case "taken":
|
|
70
|
+
return "This ID is already taken.";
|
|
71
|
+
case "reserved":
|
|
72
|
+
return "This ID is reserved.";
|
|
73
|
+
case "error":
|
|
74
|
+
return "Error checking availability.";
|
|
75
|
+
default:
|
|
76
|
+
return "Invalid Nebulon ID.";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const ensureAuthState = (config) => {
|
|
81
|
+
if (!config.auth) {
|
|
82
|
+
config.auth = {
|
|
83
|
+
token: null,
|
|
84
|
+
wallet: null,
|
|
85
|
+
handle: null,
|
|
86
|
+
role: null,
|
|
87
|
+
lastAuthAt: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const clearAuthState = (config) => {
|
|
93
|
+
config.auth = {
|
|
94
|
+
token: null,
|
|
95
|
+
wallet: null,
|
|
96
|
+
handle: null,
|
|
97
|
+
role: null,
|
|
98
|
+
lastAuthAt: null,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const validateHandleAvailability = async (baseUrl, handle) => {
|
|
103
|
+
const local = validateHandleLocally(handle);
|
|
104
|
+
if (!local.ok) {
|
|
105
|
+
return { ok: false, reason: local.reason };
|
|
106
|
+
}
|
|
107
|
+
const remote = await checkHandle(baseUrl, handle);
|
|
108
|
+
if (!remote.available) {
|
|
109
|
+
return { ok: false, reason: remote.reason || "taken" };
|
|
110
|
+
}
|
|
111
|
+
return { ok: true };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const promptForHandle = async (baseUrl) => {
|
|
115
|
+
const answer = await prompt({
|
|
116
|
+
type: "input",
|
|
117
|
+
name: "handle",
|
|
118
|
+
message: "Nebulon ID (optional)",
|
|
119
|
+
hint: "Leave blank to auto-generate",
|
|
120
|
+
validate: async (value) => {
|
|
121
|
+
const normalized = normalizeHandle(value || "");
|
|
122
|
+
if (!normalized) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const validation = await validateHandleAvailability(baseUrl, normalized);
|
|
127
|
+
if (!validation.ok) {
|
|
128
|
+
return handleReasonMessage(validation.reason);
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return handleReasonMessage("error");
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const normalized = normalizeHandle(answer.handle || "");
|
|
137
|
+
return normalized || null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const promptForPfp = async () => {
|
|
141
|
+
const answer = await prompt({
|
|
142
|
+
type: "select",
|
|
143
|
+
name: "pfp",
|
|
144
|
+
message: "Select profile picture",
|
|
145
|
+
choices: PFP_CHOICES,
|
|
146
|
+
});
|
|
147
|
+
if (!answer.pfp) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return answer.pfp.endsWith(".png") ? answer.pfp : `${answer.pfp}.png`;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const dumpActiveHandles = () => {
|
|
154
|
+
if (typeof process._getActiveHandles !== "function") {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const handles = process._getActiveHandles();
|
|
158
|
+
const requests =
|
|
159
|
+
typeof process._getActiveRequests === "function"
|
|
160
|
+
? process._getActiveRequests()
|
|
161
|
+
: [];
|
|
162
|
+
|
|
163
|
+
const summarize = (item) => {
|
|
164
|
+
if (!item) {
|
|
165
|
+
return "unknown";
|
|
166
|
+
}
|
|
167
|
+
if (item.constructor && item.constructor.name) {
|
|
168
|
+
return item.constructor.name;
|
|
169
|
+
}
|
|
170
|
+
return typeof item;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
console.log("Debug: active handles:", handles.map(summarize).join(", "));
|
|
174
|
+
if (requests.length) {
|
|
175
|
+
console.log(
|
|
176
|
+
"Debug: active requests:",
|
|
177
|
+
requests.map(summarize).join(", ")
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
console.log(
|
|
181
|
+
`Debug: stdin paused=${process.stdin.isPaused()} readable=${process.stdin.readable}`
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const finalizeLogin = (debug, exitCode = 0) => {
|
|
186
|
+
if (debug) {
|
|
187
|
+
dumpActiveHandles();
|
|
188
|
+
}
|
|
189
|
+
if (process.stdin.isTTY) {
|
|
190
|
+
try {
|
|
191
|
+
process.stdin.setRawMode(false);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// ignore
|
|
194
|
+
}
|
|
195
|
+
process.stdin.pause();
|
|
196
|
+
}
|
|
197
|
+
process.exit(exitCode);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const runLogin = async (options = {}) => {
|
|
201
|
+
const debug = Boolean(options.debug);
|
|
202
|
+
const config = loadConfig();
|
|
203
|
+
if (config.mode !== "hosted") {
|
|
204
|
+
console.log("Direct mode does not use hosted login.");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!config.activeWallet) {
|
|
208
|
+
console.error("No active wallet. Run: nebulon init");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
ensureAuthState(config);
|
|
212
|
+
const keypair = loadWalletKeypair(config, config.activeWallet);
|
|
213
|
+
const wallet = keypair.publicKey.toBase58();
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
if (config.auth.token) {
|
|
217
|
+
try {
|
|
218
|
+
const profile = await me(config.backendUrl, config.auth.token);
|
|
219
|
+
config.auth.wallet = profile.wallet;
|
|
220
|
+
config.auth.handle = profile.nebulonId || profile.handle || null;
|
|
221
|
+
config.auth.role = profile.role || null;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
clearAuthState(config);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log("Requesting challenge...");
|
|
228
|
+
const challenge = await getChallenge(config.backendUrl, wallet);
|
|
229
|
+
if (debug) {
|
|
230
|
+
console.log("Debug: challenge nonce", challenge.nonce);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log("Signing challenge...");
|
|
234
|
+
const signature = signChallenge(keypair, challenge.message);
|
|
235
|
+
if (debug) {
|
|
236
|
+
console.log("Debug: signature length", signature.length);
|
|
237
|
+
}
|
|
238
|
+
const result = await verify(config.backendUrl, {
|
|
239
|
+
wallet,
|
|
240
|
+
nonce: challenge.nonce,
|
|
241
|
+
signature,
|
|
242
|
+
});
|
|
243
|
+
if (debug) {
|
|
244
|
+
console.log("Debug: verify user", result.user);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
config.auth.token = result.token;
|
|
248
|
+
config.auth.wallet = result.user.wallet;
|
|
249
|
+
config.auth.handle = result.user.handle || null;
|
|
250
|
+
config.auth.role = result.user.role || null;
|
|
251
|
+
config.auth.lastAuthAt = nowSeconds();
|
|
252
|
+
saveConfig(config);
|
|
253
|
+
|
|
254
|
+
if (!config.auth.handle) {
|
|
255
|
+
console.log("First-time setup detected.");
|
|
256
|
+
console.log(
|
|
257
|
+
"Choose a Nebulon ID (leave blank to auto-generate) and a profile picture."
|
|
258
|
+
);
|
|
259
|
+
let desiredHandle = options.handle ? normalizeHandle(options.handle) : null;
|
|
260
|
+
if (options.handle) {
|
|
261
|
+
const validation = await validateHandleAvailability(
|
|
262
|
+
config.backendUrl,
|
|
263
|
+
desiredHandle
|
|
264
|
+
);
|
|
265
|
+
if (!validation.ok) {
|
|
266
|
+
console.error(`Handle invalid: ${handleReasonMessage(validation.reason)}`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
try {
|
|
271
|
+
desiredHandle = await promptForHandle(config.backendUrl);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (isPromptCancel(error)) {
|
|
274
|
+
console.log("Login canceled - no changes applied.");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let pfpChoice = null;
|
|
282
|
+
try {
|
|
283
|
+
pfpChoice = await promptForPfp();
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (isPromptCancel(error)) {
|
|
286
|
+
console.log("Login canceled - no changes applied.");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (debug) {
|
|
293
|
+
console.log("Debug: handle update payload", {
|
|
294
|
+
handle: desiredHandle || null,
|
|
295
|
+
pfp: pfpChoice || null,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const update = await updateHandle(
|
|
299
|
+
config.backendUrl,
|
|
300
|
+
config.auth.token,
|
|
301
|
+
desiredHandle,
|
|
302
|
+
pfpChoice
|
|
303
|
+
);
|
|
304
|
+
if (debug) {
|
|
305
|
+
console.log("Debug: handle update response", update);
|
|
306
|
+
}
|
|
307
|
+
config.auth.handle = update.handle || desiredHandle;
|
|
308
|
+
saveConfig(config);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
successMessage("Login successful.");
|
|
312
|
+
finalizeLogin(debug, 0);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
throw new Error(error && error.message ? error.message : "Login failed.");
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
runLogin,
|
|
320
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { loadConfig, saveConfig } = require("../config");
|
|
2
|
+
|
|
3
|
+
const runLogout = async () => {
|
|
4
|
+
const config = loadConfig();
|
|
5
|
+
if (!config.auth) {
|
|
6
|
+
console.log("Already logged out.");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
config.auth = {
|
|
10
|
+
token: null,
|
|
11
|
+
wallet: null,
|
|
12
|
+
handle: null,
|
|
13
|
+
role: null,
|
|
14
|
+
lastAuthAt: null,
|
|
15
|
+
};
|
|
16
|
+
saveConfig(config);
|
|
17
|
+
console.log("Logged out.");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
runLogout,
|
|
22
|
+
};
|