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
package/src/hosted.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const bs58Module = require("bs58");
|
|
2
|
+
const nacl = require("tweetnacl");
|
|
3
|
+
|
|
4
|
+
const bs58 =
|
|
5
|
+
(bs58Module && bs58Module.encode && bs58Module) ||
|
|
6
|
+
(bs58Module && bs58Module.default && bs58Module.default);
|
|
7
|
+
|
|
8
|
+
const encodeBase58 = (value) => {
|
|
9
|
+
if (!bs58 || typeof bs58.encode !== "function") {
|
|
10
|
+
throw new Error("Base58 encoder unavailable.");
|
|
11
|
+
}
|
|
12
|
+
return bs58.encode(value);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ensureUrl = (baseUrl, path) =>
|
|
16
|
+
`${baseUrl.replace(/\/$/, "")}${path}`;
|
|
17
|
+
|
|
18
|
+
const fetchJson = async (url, options = {}) => {
|
|
19
|
+
const res = await fetch(url, {
|
|
20
|
+
...options,
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
...(options.headers || {}),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
let data = null;
|
|
27
|
+
let parseError = null;
|
|
28
|
+
try {
|
|
29
|
+
data = await res.json();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
parseError = error;
|
|
32
|
+
}
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const message = (data && (data.error || data.message)) || res.statusText;
|
|
35
|
+
const error = new Error(message);
|
|
36
|
+
error.data = data;
|
|
37
|
+
error.status = res.status;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (parseError) {
|
|
41
|
+
const text = await res.text().catch(() => "");
|
|
42
|
+
const preview = text ? text.slice(0, 200).replace(/\s+/g, " ") : "n/a";
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unexpected non-JSON response from ${url} (status ${res.status}). Body: ${preview}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return data || {};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getChallenge = async (baseUrl, wallet) =>
|
|
51
|
+
fetchJson(ensureUrl(baseUrl, "/v1/auth/challenge"), {
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: JSON.stringify({ wallet }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const signChallenge = (keypair, message) => {
|
|
57
|
+
const messageBytes = Buffer.from(message, "utf8");
|
|
58
|
+
const signatureBytes = nacl.sign.detached(messageBytes, keypair.secretKey);
|
|
59
|
+
return encodeBase58(signatureBytes);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const verify = async (baseUrl, payload) =>
|
|
63
|
+
fetchJson(ensureUrl(baseUrl, "/v1/auth/verify"), {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body: JSON.stringify(payload),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const login = async (baseUrl, keypair, handle) => {
|
|
69
|
+
const wallet = keypair.publicKey.toBase58();
|
|
70
|
+
const challenge = await getChallenge(baseUrl, wallet);
|
|
71
|
+
const signature = signChallenge(keypair, challenge.message);
|
|
72
|
+
const body = {
|
|
73
|
+
wallet,
|
|
74
|
+
nonce: challenge.nonce,
|
|
75
|
+
signature,
|
|
76
|
+
};
|
|
77
|
+
if (handle) {
|
|
78
|
+
body.nebulonId = handle;
|
|
79
|
+
}
|
|
80
|
+
return verify(baseUrl, body);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const me = async (baseUrl, token) =>
|
|
84
|
+
fetchJson(ensureUrl(baseUrl, "/v1/auth/me"), {
|
|
85
|
+
method: "GET",
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${token}`,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const getContracts = async (baseUrl, token) =>
|
|
92
|
+
fetchJson(ensureUrl(baseUrl, "/v1/contracts"), {
|
|
93
|
+
method: "GET",
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${token}`,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const getContract = async (baseUrl, token, id) =>
|
|
100
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}`), {
|
|
101
|
+
method: "GET",
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: `Bearer ${token}`,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const getContractKeys = async (baseUrl, token, id) =>
|
|
108
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}/keys`), {
|
|
109
|
+
method: "GET",
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${token}`,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const updateContract = async (baseUrl, token, id, payload) =>
|
|
116
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}`), {
|
|
117
|
+
method: "PATCH",
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: `Bearer ${token}`,
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify(payload || {}),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const setContractKey = async (baseUrl, token, id, publicKey) =>
|
|
125
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}/keys`), {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: `Bearer ${token}`,
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({ publicKey }),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const lockContract = async (baseUrl, token, id) =>
|
|
134
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}/lock`), {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: {
|
|
137
|
+
Authorization: `Bearer ${token}`,
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify({}),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const signContract = async (baseUrl, token, id) =>
|
|
143
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}/sign`), {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
Authorization: `Bearer ${token}`,
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify({}),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const markFunded = async (baseUrl, token, id) =>
|
|
152
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}/mark-funded`), {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: {
|
|
155
|
+
Authorization: `Bearer ${token}`,
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({}),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const refreshContract = async (baseUrl, token, id) =>
|
|
161
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}/refresh`), {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
Authorization: `Bearer ${token}`,
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const linkEscrow = async (baseUrl, token, id, payload) =>
|
|
170
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}/link-escrow`), {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
Authorization: `Bearer ${token}`,
|
|
174
|
+
},
|
|
175
|
+
body: JSON.stringify(payload || {}),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const rateContract = async (baseUrl, token, id, score) =>
|
|
179
|
+
fetchJson(ensureUrl(baseUrl, `/v1/contracts/${id}/rate`), {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: `Bearer ${token}`,
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({ score }),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const createInvite = async (baseUrl, token, payload) =>
|
|
188
|
+
fetchJson(ensureUrl(baseUrl, "/v1/invites"), {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: {
|
|
191
|
+
Authorization: `Bearer ${token}`,
|
|
192
|
+
},
|
|
193
|
+
body: JSON.stringify(payload || {}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const getInvites = async (baseUrl, token) =>
|
|
197
|
+
fetchJson(ensureUrl(baseUrl, "/v1/invites"), {
|
|
198
|
+
method: "GET",
|
|
199
|
+
headers: {
|
|
200
|
+
Authorization: `Bearer ${token}`,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const acceptInvite = async (baseUrl, token, inviteToken) =>
|
|
205
|
+
fetchJson(ensureUrl(baseUrl, "/v1/invites/accept"), {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: {
|
|
208
|
+
Authorization: `Bearer ${token}`,
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify({ token: inviteToken }),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const acceptInviteById = async (baseUrl, token, payload) =>
|
|
214
|
+
fetchJson(ensureUrl(baseUrl, "/v1/invites/accept-id"), {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: {
|
|
217
|
+
Authorization: `Bearer ${token}`,
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify(payload || {}),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const declineInvite = async (baseUrl, token, payload) =>
|
|
223
|
+
fetchJson(ensureUrl(baseUrl, "/v1/invites/decline"), {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: {
|
|
226
|
+
Authorization: `Bearer ${token}`,
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify(payload || {}),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const checkHandle = async (baseUrl, handle) =>
|
|
232
|
+
fetchJson(
|
|
233
|
+
ensureUrl(baseUrl, `/v1/ids/check?handle=${encodeURIComponent(handle)}`)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const updateHandle = async (baseUrl, token, handle, pfp) => {
|
|
237
|
+
const payload = {};
|
|
238
|
+
if (handle) {
|
|
239
|
+
payload.nebulonId = handle;
|
|
240
|
+
}
|
|
241
|
+
if (pfp) {
|
|
242
|
+
payload.pfp = pfp;
|
|
243
|
+
}
|
|
244
|
+
return fetchJson(ensureUrl(baseUrl, "/v1/auth/handle"), {
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: {
|
|
247
|
+
Authorization: `Bearer ${token}`,
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify(payload),
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const faucet = async (baseUrl, token) =>
|
|
254
|
+
fetchJson(ensureUrl(baseUrl, "/v1/faucet/tusdc"), {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: {
|
|
257
|
+
Authorization: `Bearer ${token}`,
|
|
258
|
+
},
|
|
259
|
+
body: JSON.stringify({}),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
module.exports = {
|
|
263
|
+
getConfig: (baseUrl) => fetchJson(ensureUrl(baseUrl, "/v1/config")),
|
|
264
|
+
getChallenge,
|
|
265
|
+
signChallenge,
|
|
266
|
+
verify,
|
|
267
|
+
getContracts,
|
|
268
|
+
getContract,
|
|
269
|
+
getContractKeys,
|
|
270
|
+
updateContract,
|
|
271
|
+
setContractKey,
|
|
272
|
+
lockContract,
|
|
273
|
+
signContract,
|
|
274
|
+
markFunded,
|
|
275
|
+
refreshContract,
|
|
276
|
+
linkEscrow,
|
|
277
|
+
rateContract,
|
|
278
|
+
createInvite,
|
|
279
|
+
getInvites,
|
|
280
|
+
acceptInvite,
|
|
281
|
+
acceptInviteById,
|
|
282
|
+
declineInvite,
|
|
283
|
+
checkHandle,
|
|
284
|
+
updateHandle,
|
|
285
|
+
login,
|
|
286
|
+
me,
|
|
287
|
+
faucet,
|
|
288
|
+
};
|
package/src/nebulon.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const anchor = require("@coral-xyz/anchor");
|
|
3
|
+
const { Connection, PublicKey, SystemProgram } = require("@solana/web3.js");
|
|
4
|
+
|
|
5
|
+
const IDL_PATH = path.join(__dirname, "..", "idl", "nebulon.json");
|
|
6
|
+
const IDL = require(IDL_PATH);
|
|
7
|
+
|
|
8
|
+
const ESCROW_SEED = "escrow";
|
|
9
|
+
const MILESTONE_SEED = "milestone";
|
|
10
|
+
const PRIVATE_MILESTONE_SEED = "private-milestone";
|
|
11
|
+
const TERMS_SEED = "terms";
|
|
12
|
+
const PER_VAULT_SEED = "per-vault";
|
|
13
|
+
const DISPUTE_SEED = "dispute";
|
|
14
|
+
|
|
15
|
+
const getProgramId = (config) => new PublicKey(config.programId);
|
|
16
|
+
|
|
17
|
+
const getProvider = (endpoint, keypair, wsEndpoint) => {
|
|
18
|
+
const connection = wsEndpoint
|
|
19
|
+
? new Connection(endpoint, { commitment: "confirmed", wsEndpoint })
|
|
20
|
+
: new Connection(endpoint, "confirmed");
|
|
21
|
+
const wallet = new anchor.Wallet(keypair);
|
|
22
|
+
return new anchor.AnchorProvider(connection, wallet, {
|
|
23
|
+
commitment: "confirmed",
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getProgram = (config, keypair, options = {}) => {
|
|
28
|
+
const endpoint =
|
|
29
|
+
options.endpoint ||
|
|
30
|
+
(options.useEphemeral
|
|
31
|
+
? config.ephemeralProviderUrl || config.rpcUrl
|
|
32
|
+
: config.rpcUrl);
|
|
33
|
+
const provider = getProvider(endpoint, keypair, options.wsEndpoint);
|
|
34
|
+
const programId = getProgramId(config);
|
|
35
|
+
const idl = JSON.parse(JSON.stringify(IDL));
|
|
36
|
+
idl.address = programId.toBase58();
|
|
37
|
+
const program = new anchor.Program(idl, provider);
|
|
38
|
+
return { program, provider, connection: provider.connection, programId };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const deriveEscrowPda = (client, escrowId, programId) =>
|
|
42
|
+
PublicKey.findProgramAddressSync(
|
|
43
|
+
[
|
|
44
|
+
Buffer.from(ESCROW_SEED),
|
|
45
|
+
client.toBuffer(),
|
|
46
|
+
Buffer.from(
|
|
47
|
+
Uint8Array.from(
|
|
48
|
+
(anchor.BN.isBN && anchor.BN.isBN(escrowId)
|
|
49
|
+
? escrowId
|
|
50
|
+
: new anchor.BN(escrowId)
|
|
51
|
+
).toArray("le", 8)
|
|
52
|
+
)
|
|
53
|
+
),
|
|
54
|
+
],
|
|
55
|
+
programId
|
|
56
|
+
)[0];
|
|
57
|
+
|
|
58
|
+
const deriveMilestonePda = (escrowPda, index, programId) =>
|
|
59
|
+
PublicKey.findProgramAddressSync(
|
|
60
|
+
[
|
|
61
|
+
Buffer.from(MILESTONE_SEED),
|
|
62
|
+
escrowPda.toBuffer(),
|
|
63
|
+
Buffer.from([index]),
|
|
64
|
+
],
|
|
65
|
+
programId
|
|
66
|
+
)[0];
|
|
67
|
+
|
|
68
|
+
const derivePrivateMilestonePda = (escrowPda, index, programId) =>
|
|
69
|
+
PublicKey.findProgramAddressSync(
|
|
70
|
+
[
|
|
71
|
+
Buffer.from(PRIVATE_MILESTONE_SEED),
|
|
72
|
+
escrowPda.toBuffer(),
|
|
73
|
+
Buffer.from([index]),
|
|
74
|
+
],
|
|
75
|
+
programId
|
|
76
|
+
)[0];
|
|
77
|
+
|
|
78
|
+
const deriveTermsPda = (escrowPda, programId) =>
|
|
79
|
+
PublicKey.findProgramAddressSync(
|
|
80
|
+
[Buffer.from(TERMS_SEED), escrowPda.toBuffer()],
|
|
81
|
+
programId
|
|
82
|
+
)[0];
|
|
83
|
+
|
|
84
|
+
const derivePerVaultPda = (escrowPda, programId) =>
|
|
85
|
+
PublicKey.findProgramAddressSync(
|
|
86
|
+
[Buffer.from(PER_VAULT_SEED), escrowPda.toBuffer()],
|
|
87
|
+
programId
|
|
88
|
+
)[0];
|
|
89
|
+
|
|
90
|
+
const deriveDisputePda = (escrowPda, programId) =>
|
|
91
|
+
PublicKey.findProgramAddressSync(
|
|
92
|
+
[Buffer.from(DISPUTE_SEED), escrowPda.toBuffer()],
|
|
93
|
+
programId
|
|
94
|
+
)[0];
|
|
95
|
+
|
|
96
|
+
const textToHash = (text) => {
|
|
97
|
+
const bytes = Buffer.alloc(32);
|
|
98
|
+
const encoded = Buffer.from(text, "utf8");
|
|
99
|
+
bytes.set(encoded.subarray(0, 32));
|
|
100
|
+
return Array.from(bytes);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
IDL,
|
|
105
|
+
ESCROW_SEED,
|
|
106
|
+
MILESTONE_SEED,
|
|
107
|
+
PRIVATE_MILESTONE_SEED,
|
|
108
|
+
TERMS_SEED,
|
|
109
|
+
PER_VAULT_SEED,
|
|
110
|
+
DISPUTE_SEED,
|
|
111
|
+
SystemProgram,
|
|
112
|
+
getProgram,
|
|
113
|
+
getProgramId,
|
|
114
|
+
deriveEscrowPda,
|
|
115
|
+
deriveMilestonePda,
|
|
116
|
+
derivePrivateMilestonePda,
|
|
117
|
+
deriveTermsPda,
|
|
118
|
+
derivePerVaultPda,
|
|
119
|
+
deriveDisputePda,
|
|
120
|
+
textToHash,
|
|
121
|
+
};
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const os = require("os");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const nebulonHome = () =>
|
|
5
|
+
process.env.NEBULON_HOME || path.join(os.homedir(), ".nebulon");
|
|
6
|
+
|
|
7
|
+
const legacyConfigPath = () => path.join(nebulonHome(), "config.json");
|
|
8
|
+
const legacyWalletsDir = () => path.join(nebulonHome(), "wallets");
|
|
9
|
+
const capsulesDir = () => path.join(nebulonHome(), "capsules");
|
|
10
|
+
const activeCapsulePath = () => path.join(nebulonHome(), "active-capsule.json");
|
|
11
|
+
const capsuleRoot = (name) => path.join(capsulesDir(), name);
|
|
12
|
+
const capsuleConfigPath = (name) => path.join(capsuleRoot(name), "config.json");
|
|
13
|
+
const capsuleWalletsDir = (name) => path.join(capsuleRoot(name), "wallets");
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
nebulonHome,
|
|
17
|
+
legacyConfigPath,
|
|
18
|
+
legacyWalletsDir,
|
|
19
|
+
capsulesDir,
|
|
20
|
+
activeCapsulePath,
|
|
21
|
+
capsuleRoot,
|
|
22
|
+
capsuleConfigPath,
|
|
23
|
+
capsuleWalletsDir,
|
|
24
|
+
};
|
package/src/privacy.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const bs58Module = require("bs58");
|
|
3
|
+
const nacl = require("tweetnacl");
|
|
4
|
+
const { saveConfig } = require("./config");
|
|
5
|
+
|
|
6
|
+
const bs58 =
|
|
7
|
+
(bs58Module && bs58Module.encode && bs58Module) ||
|
|
8
|
+
(bs58Module && bs58Module.default && bs58Module.default);
|
|
9
|
+
|
|
10
|
+
const encodeBase58 = (value) => {
|
|
11
|
+
if (!bs58 || typeof bs58.encode !== "function") {
|
|
12
|
+
throw new Error("Base58 encoder unavailable.");
|
|
13
|
+
}
|
|
14
|
+
return bs58.encode(value);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const decodeBase58 = (value) => {
|
|
18
|
+
if (!bs58 || typeof bs58.decode !== "function") {
|
|
19
|
+
throw new Error("Base58 decoder unavailable.");
|
|
20
|
+
}
|
|
21
|
+
return bs58.decode(value);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isEncryptedPayload = (value) =>
|
|
25
|
+
typeof value === "string" && value.startsWith("enc:v1:");
|
|
26
|
+
|
|
27
|
+
const encodeEncryptedPayload = (nonce, ciphertext, tag) => {
|
|
28
|
+
const nonceB64 = Buffer.from(nonce).toString("base64");
|
|
29
|
+
const cipherB64 = Buffer.from(ciphertext).toString("base64");
|
|
30
|
+
const tagB64 = Buffer.from(tag).toString("base64");
|
|
31
|
+
return `enc:v1:${nonceB64}:${cipherB64}:${tagB64}`;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const decodeEncryptedPayload = (payload) => {
|
|
35
|
+
if (!isEncryptedPayload(payload)) {
|
|
36
|
+
throw new Error("Payload is not encrypted.");
|
|
37
|
+
}
|
|
38
|
+
const [, , nonceB64, cipherB64, tagB64] = payload.split(":");
|
|
39
|
+
if (!nonceB64 || !cipherB64 || !tagB64) {
|
|
40
|
+
throw new Error("Malformed encrypted payload.");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
nonce: Buffer.from(nonceB64, "base64"),
|
|
44
|
+
ciphertext: Buffer.from(cipherB64, "base64"),
|
|
45
|
+
tag: Buffer.from(tagB64, "base64"),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const deriveContractKey = (selfSecretKey58, peerPublicKey58, contractId) => {
|
|
50
|
+
const selfSecret = decodeBase58(selfSecretKey58);
|
|
51
|
+
const peerPublic = decodeBase58(peerPublicKey58);
|
|
52
|
+
const shared = nacl.scalarMult(selfSecret, peerPublic);
|
|
53
|
+
const salt = Buffer.from(String(contractId), "utf8");
|
|
54
|
+
const info = Buffer.from("nebulon:contract:v1", "utf8");
|
|
55
|
+
return crypto.hkdfSync("sha256", Buffer.from(shared), salt, info, 32);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const encryptPayload = (key, plaintext, aad) => {
|
|
59
|
+
const nonce = crypto.randomBytes(12);
|
|
60
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
|
|
61
|
+
if (aad) {
|
|
62
|
+
cipher.setAAD(Buffer.from(aad, "utf8"));
|
|
63
|
+
}
|
|
64
|
+
const ciphertext = Buffer.concat([
|
|
65
|
+
cipher.update(Buffer.from(plaintext, "utf8")),
|
|
66
|
+
cipher.final(),
|
|
67
|
+
]);
|
|
68
|
+
const tag = cipher.getAuthTag();
|
|
69
|
+
return encodeEncryptedPayload(nonce, ciphertext, tag);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const decryptPayload = (key, payload, aad) => {
|
|
73
|
+
const { nonce, ciphertext, tag } = decodeEncryptedPayload(payload);
|
|
74
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
75
|
+
if (aad) {
|
|
76
|
+
decipher.setAAD(Buffer.from(aad, "utf8"));
|
|
77
|
+
}
|
|
78
|
+
decipher.setAuthTag(tag);
|
|
79
|
+
const plaintext = Buffer.concat([
|
|
80
|
+
decipher.update(ciphertext),
|
|
81
|
+
decipher.final(),
|
|
82
|
+
]);
|
|
83
|
+
return plaintext.toString("utf8");
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const ensurePrivacyConfig = (config) => {
|
|
87
|
+
config.privacy = config.privacy || {};
|
|
88
|
+
config.privacy.contractKeys = config.privacy.contractKeys || {};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const ensureContractKeypair = (config, contractId) => {
|
|
92
|
+
if (!contractId) {
|
|
93
|
+
throw new Error("Missing contract ID for privacy keys.");
|
|
94
|
+
}
|
|
95
|
+
ensurePrivacyConfig(config);
|
|
96
|
+
const existing = config.privacy.contractKeys[contractId];
|
|
97
|
+
if (existing && existing.publicKey && existing.secretKey) {
|
|
98
|
+
return existing;
|
|
99
|
+
}
|
|
100
|
+
const keypair = nacl.box.keyPair();
|
|
101
|
+
const entry = {
|
|
102
|
+
publicKey: encodeBase58(keypair.publicKey),
|
|
103
|
+
secretKey: encodeBase58(keypair.secretKey),
|
|
104
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
105
|
+
};
|
|
106
|
+
config.privacy.contractKeys[contractId] = entry;
|
|
107
|
+
saveConfig(config);
|
|
108
|
+
return entry;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
ensureContractKeypair,
|
|
113
|
+
deriveContractKey,
|
|
114
|
+
encryptPayload,
|
|
115
|
+
decryptPayload,
|
|
116
|
+
isEncryptedPayload,
|
|
117
|
+
};
|
package/src/session.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const { loadWalletKeypair } = require("./wallets");
|
|
2
|
+
const { saveConfig } = require("./config");
|
|
3
|
+
const { getChallenge, signChallenge, verify, me } = require("./hosted");
|
|
4
|
+
|
|
5
|
+
const REFRESH_INTERVAL_SECONDS = 15 * 60;
|
|
6
|
+
|
|
7
|
+
const nowSeconds = () => Math.floor(Date.now() / 1000);
|
|
8
|
+
|
|
9
|
+
const base64UrlDecode = (value) => {
|
|
10
|
+
if (!value) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
14
|
+
const padded =
|
|
15
|
+
normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
16
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const decodeJwt = (token) => {
|
|
20
|
+
if (!token) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const parts = token.split(".");
|
|
24
|
+
if (parts.length !== 3) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const payload = base64UrlDecode(parts[1]);
|
|
29
|
+
return JSON.parse(payload);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const ensureAuthState = (config) => {
|
|
36
|
+
if (!config.auth) {
|
|
37
|
+
config.auth = {
|
|
38
|
+
token: null,
|
|
39
|
+
wallet: null,
|
|
40
|
+
handle: null,
|
|
41
|
+
role: null,
|
|
42
|
+
lastAuthAt: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const recordAuth = (config, result) => {
|
|
48
|
+
config.auth.token = result.token;
|
|
49
|
+
config.auth.wallet = result.user.wallet;
|
|
50
|
+
config.auth.handle = result.user.handle || null;
|
|
51
|
+
config.auth.role = result.user.role || null;
|
|
52
|
+
config.auth.lastAuthAt = nowSeconds();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const shouldRefresh = (config) => {
|
|
56
|
+
if (!config.auth.token) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const now = nowSeconds();
|
|
60
|
+
if (config.auth.lastAuthAt && now - config.auth.lastAuthAt >= REFRESH_INTERVAL_SECONDS) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const tokenData = decodeJwt(config.auth.token);
|
|
64
|
+
if (tokenData && tokenData.exp && tokenData.exp - now <= 60) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const refreshSession = async (config, options = {}) => {
|
|
71
|
+
const keypair = loadWalletKeypair(config, config.activeWallet);
|
|
72
|
+
const wallet = keypair.publicKey.toBase58();
|
|
73
|
+
if (!options.quiet) {
|
|
74
|
+
console.log("Refreshing session...");
|
|
75
|
+
}
|
|
76
|
+
const challenge = await getChallenge(config.backendUrl, wallet);
|
|
77
|
+
const signature = signChallenge(keypair, challenge.message);
|
|
78
|
+
const result = await verify(config.backendUrl, {
|
|
79
|
+
wallet,
|
|
80
|
+
nonce: challenge.nonce,
|
|
81
|
+
signature,
|
|
82
|
+
});
|
|
83
|
+
recordAuth(config, result);
|
|
84
|
+
saveConfig(config);
|
|
85
|
+
return result;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const ensureHostedSession = async (config, options = {}) => {
|
|
89
|
+
if (config.mode !== "hosted") {
|
|
90
|
+
throw new Error("Hosted mode required.");
|
|
91
|
+
}
|
|
92
|
+
if (!config.activeWallet) {
|
|
93
|
+
throw new Error("No active wallet. Run: nebulon init");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ensureAuthState(config);
|
|
97
|
+
if (!config.auth.lastAuthAt && config.auth.token) {
|
|
98
|
+
const tokenData = decodeJwt(config.auth.token);
|
|
99
|
+
if (tokenData && tokenData.iat) {
|
|
100
|
+
config.auth.lastAuthAt = tokenData.iat;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (shouldRefresh(config)) {
|
|
105
|
+
await refreshSession(config, options);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (options.requireHandle) {
|
|
109
|
+
let handle = config.auth.handle;
|
|
110
|
+
if (!handle && config.auth.token) {
|
|
111
|
+
try {
|
|
112
|
+
const profile = await me(config.backendUrl, config.auth.token);
|
|
113
|
+
handle = profile.nebulonId || profile.handle || null;
|
|
114
|
+
config.auth.handle = handle;
|
|
115
|
+
saveConfig(config);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// ignore and fallback to error below
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!handle) {
|
|
121
|
+
throw new Error("Nebulon ID required. Run: nebulon login");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return config;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
ensureHostedSession,
|
|
130
|
+
decodeJwt,
|
|
131
|
+
};
|