@youam/sdk 0.1.0 → 0.2.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 +149 -7
- package/dist/cli/index.cjs +2633 -0
- package/dist/index.cjs +2244 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +892 -0
- package/dist/index.d.ts +892 -0
- package/dist/index.js +2218 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -14
- package/index.js +0 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/sdk/dns-verifier.ts
|
|
34
|
+
var dns_verifier_exports = {};
|
|
35
|
+
__export(dns_verifier_exports, {
|
|
36
|
+
extractPublicKey: () => extractPublicKey,
|
|
37
|
+
generateTxtRecord: () => generateTxtRecord,
|
|
38
|
+
parseUamTxt: () => parseUamTxt,
|
|
39
|
+
queryUamTxt: () => queryUamTxt,
|
|
40
|
+
resolveKeyViaHttps: () => resolveKeyViaHttps
|
|
41
|
+
});
|
|
42
|
+
function parseUamTxt(txtValue) {
|
|
43
|
+
const tags = {};
|
|
44
|
+
for (const part of txtValue.split(";")) {
|
|
45
|
+
const trimmed = part.trim();
|
|
46
|
+
if (!trimmed) continue;
|
|
47
|
+
const eqIdx = trimmed.indexOf("=");
|
|
48
|
+
if (eqIdx !== -1) {
|
|
49
|
+
const tag = trimmed.slice(0, eqIdx).trim().toLowerCase();
|
|
50
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
51
|
+
tags[tag] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return tags;
|
|
55
|
+
}
|
|
56
|
+
function extractPublicKey(tags) {
|
|
57
|
+
const keyValue = tags["key"] ?? "";
|
|
58
|
+
if (keyValue.startsWith("ed25519:")) {
|
|
59
|
+
return keyValue.slice("ed25519:".length);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
async function queryUamTxt(domain, timeout) {
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timer = timeout != null ? setTimeout(() => controller.abort(), timeout) : void 0;
|
|
66
|
+
try {
|
|
67
|
+
const records = await (0, import_promises.resolveTxt)(`_uam.${domain}`);
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const parts of records) {
|
|
70
|
+
const txtValue = parts.join("");
|
|
71
|
+
if (txtValue.trim().startsWith("v=uam1")) {
|
|
72
|
+
results.push(txtValue);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
} finally {
|
|
79
|
+
if (timer != null) clearTimeout(timer);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function resolveKeyViaHttps(agentName, domain, timeout) {
|
|
83
|
+
const url = `https://${domain}/.well-known/uam.json`;
|
|
84
|
+
try {
|
|
85
|
+
const resp = await fetch(url, {
|
|
86
|
+
signal: AbortSignal.timeout(timeout ?? 1e4)
|
|
87
|
+
});
|
|
88
|
+
if (!resp.ok) return null;
|
|
89
|
+
const data = await resp.json();
|
|
90
|
+
if (data["v"] !== "uam1") return null;
|
|
91
|
+
const agents = data["agents"] ?? {};
|
|
92
|
+
const entry = agents[agentName];
|
|
93
|
+
if (!entry) return null;
|
|
94
|
+
let keyValue = entry["key"] ?? "";
|
|
95
|
+
if (keyValue.startsWith("ed25519:")) {
|
|
96
|
+
keyValue = keyValue.slice("ed25519:".length);
|
|
97
|
+
}
|
|
98
|
+
return keyValue || null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function generateTxtRecord(publicKey, relayUrl) {
|
|
104
|
+
return `v=uam1; key=ed25519:${publicKey}; relay=${relayUrl}`;
|
|
105
|
+
}
|
|
106
|
+
var import_promises;
|
|
107
|
+
var init_dns_verifier = __esm({
|
|
108
|
+
"src/sdk/dns-verifier.ts"() {
|
|
109
|
+
"use strict";
|
|
110
|
+
import_promises = require("dns/promises");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// src/index.ts
|
|
115
|
+
var index_exports = {};
|
|
116
|
+
__export(index_exports, {
|
|
117
|
+
Agent: () => Agent,
|
|
118
|
+
protocol: () => protocol_exports
|
|
119
|
+
});
|
|
120
|
+
module.exports = __toCommonJS(index_exports);
|
|
121
|
+
|
|
122
|
+
// src/protocol/index.ts
|
|
123
|
+
var protocol_exports = {};
|
|
124
|
+
__export(protocol_exports, {
|
|
125
|
+
DecryptionError: () => DecryptionError,
|
|
126
|
+
EncryptionError: () => EncryptionError,
|
|
127
|
+
EnvelopeTooLargeError: () => EnvelopeTooLargeError,
|
|
128
|
+
InvalidAddressError: () => InvalidAddressError,
|
|
129
|
+
InvalidContactCardError: () => InvalidContactCardError,
|
|
130
|
+
InvalidEnvelopeError: () => InvalidEnvelopeError,
|
|
131
|
+
KeyPinningError: () => KeyPinningError,
|
|
132
|
+
MAX_ENVELOPE_SIZE: () => MAX_ENVELOPE_SIZE,
|
|
133
|
+
MessageType: () => MessageType,
|
|
134
|
+
SignatureError: () => SignatureError,
|
|
135
|
+
SignatureVerificationError: () => SignatureVerificationError,
|
|
136
|
+
UAMError: () => UAMError,
|
|
137
|
+
UAM_VERSION: () => UAM_VERSION,
|
|
138
|
+
b64Decode: () => b64Decode,
|
|
139
|
+
b64Encode: () => b64Encode,
|
|
140
|
+
canonicalize: () => canonicalize,
|
|
141
|
+
contactCardFromDict: () => contactCardFromDict,
|
|
142
|
+
contactCardToDict: () => contactCardToDict,
|
|
143
|
+
createContactCard: () => createContactCard,
|
|
144
|
+
createEnvelope: () => createEnvelope,
|
|
145
|
+
decryptPayload: () => decryptPayload,
|
|
146
|
+
decryptPayloadAnonymous: () => decryptPayloadAnonymous,
|
|
147
|
+
deserializeSigningKey: () => deserializeSigningKey,
|
|
148
|
+
deserializeVerifyKey: () => deserializeVerifyKey,
|
|
149
|
+
encryptPayload: () => encryptPayload,
|
|
150
|
+
encryptPayloadAnonymous: () => encryptPayloadAnonymous,
|
|
151
|
+
fromWireDict: () => fromWireDict,
|
|
152
|
+
generateKeypair: () => generateKeypair,
|
|
153
|
+
generateNonce: () => generateNonce,
|
|
154
|
+
parseAddress: () => parseAddress,
|
|
155
|
+
publicKeyFingerprint: () => publicKeyFingerprint,
|
|
156
|
+
serializeSigningKey: () => serializeSigningKey,
|
|
157
|
+
serializeVerifyKey: () => serializeVerifyKey,
|
|
158
|
+
signMessage: () => signMessage,
|
|
159
|
+
sodiumReady: () => sodiumReady,
|
|
160
|
+
toWireDict: () => toWireDict,
|
|
161
|
+
utcTimestamp: () => utcTimestamp,
|
|
162
|
+
validateEnvelopeSize: () => validateEnvelopeSize,
|
|
163
|
+
verifyContactCard: () => verifyContactCard,
|
|
164
|
+
verifyEnvelope: () => verifyEnvelope,
|
|
165
|
+
verifySignature: () => verifySignature
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// src/protocol/types.ts
|
|
169
|
+
var UAM_VERSION = "0.1";
|
|
170
|
+
var MAX_ENVELOPE_SIZE = 65536;
|
|
171
|
+
var MessageType = /* @__PURE__ */ ((MessageType2) => {
|
|
172
|
+
MessageType2["MESSAGE"] = "message";
|
|
173
|
+
MessageType2["HANDSHAKE_REQUEST"] = "handshake.request";
|
|
174
|
+
MessageType2["HANDSHAKE_ACCEPT"] = "handshake.accept";
|
|
175
|
+
MessageType2["HANDSHAKE_DENY"] = "handshake.deny";
|
|
176
|
+
MessageType2["RECEIPT_DELIVERED"] = "receipt.delivered";
|
|
177
|
+
MessageType2["RECEIPT_READ"] = "receipt.read";
|
|
178
|
+
MessageType2["RECEIPT_FAILED"] = "receipt.failed";
|
|
179
|
+
MessageType2["SESSION_REQUEST"] = "session.request";
|
|
180
|
+
MessageType2["SESSION_ACCEPT"] = "session.accept";
|
|
181
|
+
MessageType2["SESSION_DECLINE"] = "session.decline";
|
|
182
|
+
MessageType2["SESSION_END"] = "session.end";
|
|
183
|
+
return MessageType2;
|
|
184
|
+
})(MessageType || {});
|
|
185
|
+
function b64Encode(data) {
|
|
186
|
+
return Buffer.from(data).toString("base64url");
|
|
187
|
+
}
|
|
188
|
+
function b64Decode(s) {
|
|
189
|
+
return new Uint8Array(Buffer.from(s, "base64url"));
|
|
190
|
+
}
|
|
191
|
+
function utcTimestamp() {
|
|
192
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/protocol/errors.ts
|
|
196
|
+
var UAMError = class extends Error {
|
|
197
|
+
constructor(message) {
|
|
198
|
+
super(message);
|
|
199
|
+
this.name = "UAMError";
|
|
200
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var InvalidAddressError = class extends UAMError {
|
|
204
|
+
constructor(message) {
|
|
205
|
+
super(message);
|
|
206
|
+
this.name = "InvalidAddressError";
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
var InvalidEnvelopeError = class extends UAMError {
|
|
210
|
+
constructor(message) {
|
|
211
|
+
super(message);
|
|
212
|
+
this.name = "InvalidEnvelopeError";
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
var EnvelopeTooLargeError = class extends InvalidEnvelopeError {
|
|
216
|
+
constructor(message) {
|
|
217
|
+
super(message);
|
|
218
|
+
this.name = "EnvelopeTooLargeError";
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
var SignatureError = class extends UAMError {
|
|
222
|
+
constructor(message) {
|
|
223
|
+
super(message);
|
|
224
|
+
this.name = "SignatureError";
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var SignatureVerificationError = class extends SignatureError {
|
|
228
|
+
constructor(message) {
|
|
229
|
+
super(message);
|
|
230
|
+
this.name = "SignatureVerificationError";
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var EncryptionError = class extends UAMError {
|
|
234
|
+
constructor(message) {
|
|
235
|
+
super(message);
|
|
236
|
+
this.name = "EncryptionError";
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
var DecryptionError = class extends EncryptionError {
|
|
240
|
+
constructor(message) {
|
|
241
|
+
super(message);
|
|
242
|
+
this.name = "DecryptionError";
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
var InvalidContactCardError = class extends UAMError {
|
|
246
|
+
constructor(message) {
|
|
247
|
+
super(message);
|
|
248
|
+
this.name = "InvalidContactCardError";
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
var KeyPinningError = class _KeyPinningError extends UAMError {
|
|
252
|
+
constructor(message) {
|
|
253
|
+
super(message);
|
|
254
|
+
this.name = "KeyPinningError";
|
|
255
|
+
Object.setPrototypeOf(this, _KeyPinningError.prototype);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/protocol/address.ts
|
|
260
|
+
var ADDRESS_RE = /^(?<agent>[a-z0-9][a-z0-9_-]{0,62}[a-z0-9]|[a-z0-9])::(?<domain>[a-z0-9](?:[a-z0-9.-]{0,253}[a-z0-9])?)$/;
|
|
261
|
+
var MAX_AGENT_LEN = 64;
|
|
262
|
+
var MAX_ADDRESS_LEN = 128;
|
|
263
|
+
function parseAddress(raw) {
|
|
264
|
+
const normalized = raw.trim().toLowerCase();
|
|
265
|
+
if (normalized.length > MAX_ADDRESS_LEN) {
|
|
266
|
+
throw new InvalidAddressError(
|
|
267
|
+
`Address exceeds ${MAX_ADDRESS_LEN} characters: ${JSON.stringify(raw)}`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const m = ADDRESS_RE.exec(normalized);
|
|
271
|
+
if (!m || !m.groups) {
|
|
272
|
+
throw new InvalidAddressError(
|
|
273
|
+
`Invalid UAM address: ${JSON.stringify(raw)}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const agent = m.groups.agent;
|
|
277
|
+
if (agent.length > MAX_AGENT_LEN) {
|
|
278
|
+
throw new InvalidAddressError(
|
|
279
|
+
`Agent name exceeds ${MAX_AGENT_LEN} characters: ${JSON.stringify(raw)}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
agent,
|
|
284
|
+
domain: m.groups.domain,
|
|
285
|
+
get full() {
|
|
286
|
+
return `${this.agent}::${this.domain}`;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/protocol/crypto.ts
|
|
292
|
+
var import_libsodium_wrappers = __toESM(require("libsodium-wrappers"), 1);
|
|
293
|
+
var import_node_crypto = require("crypto");
|
|
294
|
+
var sodiumReady = import_libsodium_wrappers.default.ready;
|
|
295
|
+
function generateKeypair() {
|
|
296
|
+
const seed = import_libsodium_wrappers.default.randombytes_buf(32);
|
|
297
|
+
const kp = import_libsodium_wrappers.default.crypto_sign_seed_keypair(seed);
|
|
298
|
+
return {
|
|
299
|
+
signingKey: kp.privateKey,
|
|
300
|
+
verifyKey: kp.publicKey,
|
|
301
|
+
seed
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function serializeSigningKey(seed) {
|
|
305
|
+
return b64Encode(seed);
|
|
306
|
+
}
|
|
307
|
+
function deserializeSigningKey(s) {
|
|
308
|
+
const seed = b64Decode(s);
|
|
309
|
+
const kp = import_libsodium_wrappers.default.crypto_sign_seed_keypair(seed);
|
|
310
|
+
return {
|
|
311
|
+
signingKey: kp.privateKey,
|
|
312
|
+
verifyKey: kp.publicKey,
|
|
313
|
+
seed
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function serializeVerifyKey(key) {
|
|
317
|
+
return b64Encode(key);
|
|
318
|
+
}
|
|
319
|
+
function deserializeVerifyKey(s) {
|
|
320
|
+
return b64Decode(s);
|
|
321
|
+
}
|
|
322
|
+
function publicKeyFingerprint(verifyKey) {
|
|
323
|
+
return (0, import_node_crypto.createHash)("sha256").update(Buffer.from(verifyKey)).digest("hex");
|
|
324
|
+
}
|
|
325
|
+
function sortedStringify(value) {
|
|
326
|
+
if (value === null || value === void 0) {
|
|
327
|
+
return "null";
|
|
328
|
+
}
|
|
329
|
+
if (typeof value === "string") {
|
|
330
|
+
return JSON.stringify(value);
|
|
331
|
+
}
|
|
332
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
333
|
+
return String(value);
|
|
334
|
+
}
|
|
335
|
+
if (Array.isArray(value)) {
|
|
336
|
+
const items = value.map((v) => sortedStringify(v));
|
|
337
|
+
return "[" + items.join(",") + "]";
|
|
338
|
+
}
|
|
339
|
+
if (typeof value === "object") {
|
|
340
|
+
const obj = value;
|
|
341
|
+
const keys = Object.keys(obj).sort();
|
|
342
|
+
const pairs = keys.map((k) => JSON.stringify(k) + ":" + sortedStringify(obj[k]));
|
|
343
|
+
return "{" + pairs.join(",") + "}";
|
|
344
|
+
}
|
|
345
|
+
return String(value);
|
|
346
|
+
}
|
|
347
|
+
function canonicalize(data) {
|
|
348
|
+
const filtered = {};
|
|
349
|
+
for (const [k, v] of Object.entries(data)) {
|
|
350
|
+
if (k !== "signature" && v !== null && v !== void 0) {
|
|
351
|
+
filtered[k] = v;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const json = sortedStringify(filtered);
|
|
355
|
+
return new TextEncoder().encode(json);
|
|
356
|
+
}
|
|
357
|
+
function signMessage(data, signingKey) {
|
|
358
|
+
const signature = import_libsodium_wrappers.default.crypto_sign_detached(data, signingKey);
|
|
359
|
+
return b64Encode(signature);
|
|
360
|
+
}
|
|
361
|
+
function verifySignature(data, signatureB64, verifyKey) {
|
|
362
|
+
try {
|
|
363
|
+
const sigBytes = b64Decode(signatureB64);
|
|
364
|
+
const valid = import_libsodium_wrappers.default.crypto_sign_verify_detached(sigBytes, data, verifyKey);
|
|
365
|
+
if (!valid) {
|
|
366
|
+
throw new SignatureVerificationError("Signature verification failed");
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
if (err instanceof SignatureVerificationError) {
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
372
|
+
throw new SignatureVerificationError(
|
|
373
|
+
err instanceof Error ? err.message : String(err)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function generateNonce() {
|
|
378
|
+
return b64Encode(import_libsodium_wrappers.default.randombytes_buf(24));
|
|
379
|
+
}
|
|
380
|
+
function encryptPayload(plaintext, senderSigningKey, recipientVerifyKey) {
|
|
381
|
+
try {
|
|
382
|
+
const senderCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_sk_to_curve25519(senderSigningKey);
|
|
383
|
+
const recipientCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_pk_to_curve25519(recipientVerifyKey);
|
|
384
|
+
const nonce = import_libsodium_wrappers.default.randombytes_buf(import_libsodium_wrappers.default.crypto_box_NONCEBYTES);
|
|
385
|
+
const encrypted = import_libsodium_wrappers.default.crypto_box_easy(plaintext, nonce, recipientCurve, senderCurve);
|
|
386
|
+
const combined = new Uint8Array(nonce.length + encrypted.length);
|
|
387
|
+
combined.set(nonce, 0);
|
|
388
|
+
combined.set(encrypted, nonce.length);
|
|
389
|
+
return b64Encode(combined);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (err instanceof EncryptionError) throw err;
|
|
392
|
+
throw new EncryptionError(err instanceof Error ? err.message : String(err));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function decryptPayload(ciphertextB64, recipientSigningKey, senderVerifyKey) {
|
|
396
|
+
try {
|
|
397
|
+
const combined = b64Decode(ciphertextB64);
|
|
398
|
+
const nonce = combined.slice(0, import_libsodium_wrappers.default.crypto_box_NONCEBYTES);
|
|
399
|
+
const ciphertext = combined.slice(import_libsodium_wrappers.default.crypto_box_NONCEBYTES);
|
|
400
|
+
const recipientCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_sk_to_curve25519(recipientSigningKey);
|
|
401
|
+
const senderCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_pk_to_curve25519(senderVerifyKey);
|
|
402
|
+
return import_libsodium_wrappers.default.crypto_box_open_easy(ciphertext, nonce, senderCurve, recipientCurve);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (err instanceof DecryptionError) throw err;
|
|
405
|
+
throw new DecryptionError(err instanceof Error ? err.message : String(err));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function encryptPayloadAnonymous(plaintext, recipientVerifyKey) {
|
|
409
|
+
const recipientCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_pk_to_curve25519(recipientVerifyKey);
|
|
410
|
+
const ciphertext = import_libsodium_wrappers.default.crypto_box_seal(plaintext, recipientCurve);
|
|
411
|
+
return b64Encode(ciphertext);
|
|
412
|
+
}
|
|
413
|
+
function decryptPayloadAnonymous(ciphertextB64, recipientSigningKey) {
|
|
414
|
+
try {
|
|
415
|
+
const ciphertext = b64Decode(ciphertextB64);
|
|
416
|
+
const recipientCurveSk = import_libsodium_wrappers.default.crypto_sign_ed25519_sk_to_curve25519(recipientSigningKey);
|
|
417
|
+
const recipientVerifyKey = recipientSigningKey.slice(32);
|
|
418
|
+
const recipientCurvePk = import_libsodium_wrappers.default.crypto_sign_ed25519_pk_to_curve25519(recipientVerifyKey);
|
|
419
|
+
return import_libsodium_wrappers.default.crypto_box_seal_open(ciphertext, recipientCurvePk, recipientCurveSk);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
if (err instanceof DecryptionError) throw err;
|
|
422
|
+
throw new DecryptionError(err instanceof Error ? err.message : String(err));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/protocol/envelope.ts
|
|
427
|
+
var import_node_crypto2 = require("crypto");
|
|
428
|
+
var REQUIRED_WIRE_FIELDS = /* @__PURE__ */ new Set([
|
|
429
|
+
"uam_version",
|
|
430
|
+
"message_id",
|
|
431
|
+
"from",
|
|
432
|
+
"to",
|
|
433
|
+
"timestamp",
|
|
434
|
+
"type",
|
|
435
|
+
"nonce",
|
|
436
|
+
"payload",
|
|
437
|
+
"signature"
|
|
438
|
+
]);
|
|
439
|
+
function buildSignableDict(envelope) {
|
|
440
|
+
const d = {
|
|
441
|
+
uam_version: envelope.uamVersion,
|
|
442
|
+
message_id: envelope.messageId,
|
|
443
|
+
from: envelope.fromAddress,
|
|
444
|
+
to: envelope.toAddress,
|
|
445
|
+
timestamp: envelope.timestamp,
|
|
446
|
+
type: envelope.type,
|
|
447
|
+
nonce: envelope.nonce,
|
|
448
|
+
payload: envelope.payload
|
|
449
|
+
};
|
|
450
|
+
if (envelope.threadId !== void 0) d["thread_id"] = envelope.threadId;
|
|
451
|
+
if (envelope.replyTo !== void 0) d["reply_to"] = envelope.replyTo;
|
|
452
|
+
if (envelope.expires !== void 0) d["expires"] = envelope.expires;
|
|
453
|
+
if (envelope.mediaType !== void 0) d["media_type"] = envelope.mediaType;
|
|
454
|
+
if (envelope.metadata !== void 0) d["metadata"] = envelope.metadata;
|
|
455
|
+
return d;
|
|
456
|
+
}
|
|
457
|
+
function toWireDict(envelope) {
|
|
458
|
+
const d = buildSignableDict(envelope);
|
|
459
|
+
d["signature"] = envelope.signature;
|
|
460
|
+
if (envelope.attachments !== void 0) {
|
|
461
|
+
d["attachments"] = envelope.attachments;
|
|
462
|
+
}
|
|
463
|
+
return d;
|
|
464
|
+
}
|
|
465
|
+
function fromWireDict(d) {
|
|
466
|
+
const keys = new Set(Object.keys(d));
|
|
467
|
+
const missing = [];
|
|
468
|
+
for (const field of REQUIRED_WIRE_FIELDS) {
|
|
469
|
+
if (!keys.has(field)) missing.push(field);
|
|
470
|
+
}
|
|
471
|
+
if (missing.length > 0) {
|
|
472
|
+
missing.sort();
|
|
473
|
+
throw new InvalidEnvelopeError(
|
|
474
|
+
`Missing required fields: ${JSON.stringify(missing)}`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return Object.freeze({
|
|
478
|
+
uamVersion: d["uam_version"],
|
|
479
|
+
messageId: d["message_id"],
|
|
480
|
+
fromAddress: d["from"],
|
|
481
|
+
toAddress: d["to"],
|
|
482
|
+
timestamp: d["timestamp"],
|
|
483
|
+
type: d["type"],
|
|
484
|
+
nonce: d["nonce"],
|
|
485
|
+
payload: d["payload"],
|
|
486
|
+
signature: d["signature"],
|
|
487
|
+
threadId: d["thread_id"],
|
|
488
|
+
replyTo: d["reply_to"],
|
|
489
|
+
expires: d["expires"],
|
|
490
|
+
mediaType: d["media_type"],
|
|
491
|
+
metadata: d["metadata"],
|
|
492
|
+
attachments: d["attachments"]
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function validateEnvelopeSize(envelope) {
|
|
496
|
+
const wire = toWireDict(envelope);
|
|
497
|
+
const json = JSON.stringify(wire);
|
|
498
|
+
const size = new TextEncoder().encode(json).length;
|
|
499
|
+
if (size > MAX_ENVELOPE_SIZE) {
|
|
500
|
+
throw new EnvelopeTooLargeError(
|
|
501
|
+
`Envelope size ${size} bytes exceeds maximum ${MAX_ENVELOPE_SIZE} bytes`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaintext, signingKey, recipientVerifyKey, options) {
|
|
506
|
+
const typeValue = typeof messageType === "string" ? messageType : messageType;
|
|
507
|
+
parseAddress(fromAddress);
|
|
508
|
+
parseAddress(toAddress);
|
|
509
|
+
const messageId = (0, import_node_crypto2.randomUUID)();
|
|
510
|
+
const nonce = generateNonce();
|
|
511
|
+
const timestamp = utcTimestamp();
|
|
512
|
+
let encryptedPayload;
|
|
513
|
+
if (typeValue === "handshake.request" /* HANDSHAKE_REQUEST */) {
|
|
514
|
+
encryptedPayload = encryptPayloadAnonymous(payloadPlaintext, recipientVerifyKey);
|
|
515
|
+
} else {
|
|
516
|
+
encryptedPayload = encryptPayload(
|
|
517
|
+
payloadPlaintext,
|
|
518
|
+
signingKey,
|
|
519
|
+
recipientVerifyKey
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
const tempEnvelope = {
|
|
523
|
+
uamVersion: UAM_VERSION,
|
|
524
|
+
messageId,
|
|
525
|
+
fromAddress,
|
|
526
|
+
toAddress,
|
|
527
|
+
timestamp,
|
|
528
|
+
type: typeValue,
|
|
529
|
+
nonce,
|
|
530
|
+
payload: encryptedPayload,
|
|
531
|
+
signature: "",
|
|
532
|
+
threadId: options?.threadId,
|
|
533
|
+
replyTo: options?.replyTo,
|
|
534
|
+
expires: options?.expires,
|
|
535
|
+
mediaType: options?.mediaType,
|
|
536
|
+
metadata: options?.metadata,
|
|
537
|
+
attachments: options?.attachments
|
|
538
|
+
};
|
|
539
|
+
const signable = buildSignableDict(tempEnvelope);
|
|
540
|
+
const signature = signMessage(canonicalize(signable), signingKey);
|
|
541
|
+
const finalEnvelope = Object.freeze({
|
|
542
|
+
uamVersion: UAM_VERSION,
|
|
543
|
+
messageId,
|
|
544
|
+
fromAddress,
|
|
545
|
+
toAddress,
|
|
546
|
+
timestamp,
|
|
547
|
+
type: typeValue,
|
|
548
|
+
nonce,
|
|
549
|
+
payload: encryptedPayload,
|
|
550
|
+
signature,
|
|
551
|
+
threadId: options?.threadId,
|
|
552
|
+
replyTo: options?.replyTo,
|
|
553
|
+
expires: options?.expires,
|
|
554
|
+
mediaType: options?.mediaType,
|
|
555
|
+
metadata: options?.metadata,
|
|
556
|
+
attachments: options?.attachments
|
|
557
|
+
});
|
|
558
|
+
validateEnvelopeSize(finalEnvelope);
|
|
559
|
+
return finalEnvelope;
|
|
560
|
+
}
|
|
561
|
+
function verifyEnvelope(envelope, senderVerifyKey) {
|
|
562
|
+
const signable = buildSignableDict(envelope);
|
|
563
|
+
verifySignature(canonicalize(signable), envelope.signature, senderVerifyKey);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/protocol/contact.ts
|
|
567
|
+
function buildSignableDict2(card) {
|
|
568
|
+
const d = {
|
|
569
|
+
version: card.version,
|
|
570
|
+
address: card.address,
|
|
571
|
+
display_name: card.displayName,
|
|
572
|
+
relay: card.relay,
|
|
573
|
+
public_key: card.publicKey
|
|
574
|
+
};
|
|
575
|
+
if (card.description !== void 0) d["description"] = card.description;
|
|
576
|
+
if (card.system !== void 0) d["system"] = card.system;
|
|
577
|
+
if (card.connectionEndpoint !== void 0) {
|
|
578
|
+
d["connection_endpoint"] = card.connectionEndpoint;
|
|
579
|
+
}
|
|
580
|
+
if (card.verifiedDomain !== void 0) {
|
|
581
|
+
d["verified_domain"] = card.verifiedDomain;
|
|
582
|
+
}
|
|
583
|
+
return d;
|
|
584
|
+
}
|
|
585
|
+
function contactCardToDict(card) {
|
|
586
|
+
const d = buildSignableDict2(card);
|
|
587
|
+
d["signature"] = card.signature;
|
|
588
|
+
if (card.payloadFormats !== void 0) {
|
|
589
|
+
d["payload_formats"] = card.payloadFormats;
|
|
590
|
+
}
|
|
591
|
+
if (card.fingerprint !== void 0) {
|
|
592
|
+
d["fingerprint"] = card.fingerprint;
|
|
593
|
+
}
|
|
594
|
+
if (card.relays !== void 0) {
|
|
595
|
+
d["relays"] = card.relays;
|
|
596
|
+
}
|
|
597
|
+
return d;
|
|
598
|
+
}
|
|
599
|
+
function contactCardFromDict(d, options) {
|
|
600
|
+
const verify = options?.verify ?? true;
|
|
601
|
+
const required = /* @__PURE__ */ new Set([
|
|
602
|
+
"version",
|
|
603
|
+
"address",
|
|
604
|
+
"display_name",
|
|
605
|
+
"relay",
|
|
606
|
+
"public_key",
|
|
607
|
+
"signature"
|
|
608
|
+
]);
|
|
609
|
+
const keys = new Set(Object.keys(d));
|
|
610
|
+
const missing = [];
|
|
611
|
+
for (const field of required) {
|
|
612
|
+
if (!keys.has(field)) missing.push(field);
|
|
613
|
+
}
|
|
614
|
+
if (missing.length > 0) {
|
|
615
|
+
missing.sort();
|
|
616
|
+
throw new InvalidContactCardError(
|
|
617
|
+
`Missing required fields: ${JSON.stringify(missing)}`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
const card = Object.freeze({
|
|
621
|
+
version: d["version"],
|
|
622
|
+
address: d["address"],
|
|
623
|
+
displayName: d["display_name"],
|
|
624
|
+
description: d["description"],
|
|
625
|
+
system: d["system"],
|
|
626
|
+
connectionEndpoint: d["connection_endpoint"],
|
|
627
|
+
relay: d["relay"],
|
|
628
|
+
publicKey: d["public_key"],
|
|
629
|
+
signature: d["signature"],
|
|
630
|
+
verifiedDomain: d["verified_domain"],
|
|
631
|
+
payloadFormats: d["payload_formats"],
|
|
632
|
+
fingerprint: d["fingerprint"],
|
|
633
|
+
relays: d["relays"]
|
|
634
|
+
});
|
|
635
|
+
if (verify) {
|
|
636
|
+
verifyContactCard(card);
|
|
637
|
+
}
|
|
638
|
+
return card;
|
|
639
|
+
}
|
|
640
|
+
async function createContactCard(address, displayName, relay, signingKey, options) {
|
|
641
|
+
parseAddress(address);
|
|
642
|
+
const verifyKey = signingKey.slice(32);
|
|
643
|
+
const pubKeyB64 = serializeVerifyKey(verifyKey);
|
|
644
|
+
const fp = publicKeyFingerprint(verifyKey);
|
|
645
|
+
const payloadFormats = options?.payloadFormats ?? [
|
|
646
|
+
"text/plain",
|
|
647
|
+
"text/markdown"
|
|
648
|
+
];
|
|
649
|
+
const tempCard = {
|
|
650
|
+
version: UAM_VERSION,
|
|
651
|
+
address,
|
|
652
|
+
displayName,
|
|
653
|
+
description: options?.description,
|
|
654
|
+
system: options?.system,
|
|
655
|
+
connectionEndpoint: options?.connectionEndpoint,
|
|
656
|
+
relay,
|
|
657
|
+
publicKey: pubKeyB64,
|
|
658
|
+
signature: "",
|
|
659
|
+
verifiedDomain: options?.verifiedDomain,
|
|
660
|
+
payloadFormats,
|
|
661
|
+
fingerprint: fp,
|
|
662
|
+
relays: options?.relays
|
|
663
|
+
};
|
|
664
|
+
const signable = buildSignableDict2(tempCard);
|
|
665
|
+
const signature = signMessage(canonicalize(signable), signingKey);
|
|
666
|
+
return Object.freeze({
|
|
667
|
+
version: UAM_VERSION,
|
|
668
|
+
address,
|
|
669
|
+
displayName,
|
|
670
|
+
description: options?.description,
|
|
671
|
+
system: options?.system,
|
|
672
|
+
connectionEndpoint: options?.connectionEndpoint,
|
|
673
|
+
relay,
|
|
674
|
+
publicKey: pubKeyB64,
|
|
675
|
+
signature,
|
|
676
|
+
verifiedDomain: options?.verifiedDomain,
|
|
677
|
+
payloadFormats,
|
|
678
|
+
fingerprint: fp,
|
|
679
|
+
relays: options?.relays
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
function verifyContactCard(card) {
|
|
683
|
+
try {
|
|
684
|
+
parseAddress(card.address);
|
|
685
|
+
} catch (err) {
|
|
686
|
+
throw new InvalidContactCardError(
|
|
687
|
+
`Invalid address in contact card: ${err instanceof Error ? err.message : String(err)}`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
const vk = deserializeVerifyKey(card.publicKey);
|
|
691
|
+
const signable = buildSignableDict2(card);
|
|
692
|
+
verifySignature(canonicalize(signable), card.signature, vk);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/sdk/config.ts
|
|
696
|
+
var import_node_os = require("os");
|
|
697
|
+
var import_node_path = require("path");
|
|
698
|
+
var DEFAULT_RELAY_URL = "https://relay.youam.network";
|
|
699
|
+
var VALID_POLICIES = /* @__PURE__ */ new Set([
|
|
700
|
+
"auto-accept",
|
|
701
|
+
"approval-required",
|
|
702
|
+
"allowlist-only",
|
|
703
|
+
"require_verify"
|
|
704
|
+
]);
|
|
705
|
+
var SDKConfig = class {
|
|
706
|
+
name;
|
|
707
|
+
relayUrl;
|
|
708
|
+
relayWsUrl;
|
|
709
|
+
keyDir;
|
|
710
|
+
dataDir;
|
|
711
|
+
displayName;
|
|
712
|
+
transportType;
|
|
713
|
+
trustPolicy;
|
|
714
|
+
relayDomain;
|
|
715
|
+
constructor(options) {
|
|
716
|
+
this.name = options.name;
|
|
717
|
+
this.relayUrl = options.relayUrl ?? process.env["UAM_RELAY_URL"] ?? DEFAULT_RELAY_URL;
|
|
718
|
+
if (options.relayWsUrl) {
|
|
719
|
+
this.relayWsUrl = options.relayWsUrl;
|
|
720
|
+
} else {
|
|
721
|
+
let wsUrl = this.relayUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
722
|
+
if (!wsUrl.endsWith("/ws")) {
|
|
723
|
+
wsUrl = wsUrl.replace(/\/+$/, "") + "/ws";
|
|
724
|
+
}
|
|
725
|
+
this.relayWsUrl = wsUrl;
|
|
726
|
+
}
|
|
727
|
+
const envDomain = process.env["UAM_RELAY_DOMAIN"];
|
|
728
|
+
if (envDomain) {
|
|
729
|
+
this.relayDomain = envDomain;
|
|
730
|
+
} else if (options.relayDomain) {
|
|
731
|
+
this.relayDomain = options.relayDomain;
|
|
732
|
+
} else {
|
|
733
|
+
try {
|
|
734
|
+
const parsed = new URL(this.relayUrl);
|
|
735
|
+
this.relayDomain = parsed.hostname || "localhost";
|
|
736
|
+
} catch {
|
|
737
|
+
const afterScheme = this.relayUrl.split("://").pop() || "";
|
|
738
|
+
this.relayDomain = afterScheme.split("/")[0] || "localhost";
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const uamHome = process.env["UAM_HOME"];
|
|
742
|
+
const defaultHome = uamHome || (0, import_node_path.join)((0, import_node_os.homedir)(), ".uam");
|
|
743
|
+
this.keyDir = options.keyDir ?? (0, import_node_path.join)(defaultHome, "keys");
|
|
744
|
+
this.dataDir = options.dataDir ?? defaultHome;
|
|
745
|
+
this.displayName = options.displayName ?? this.name;
|
|
746
|
+
this.transportType = options.transportType ?? "websocket";
|
|
747
|
+
let policy = options.trustPolicy ?? "auto-accept";
|
|
748
|
+
const envPolicy = process.env["UAM_TRUST_POLICY"];
|
|
749
|
+
if (envPolicy) {
|
|
750
|
+
policy = envPolicy;
|
|
751
|
+
}
|
|
752
|
+
if (!VALID_POLICIES.has(policy)) {
|
|
753
|
+
throw new Error(
|
|
754
|
+
`Invalid trust_policy '${policy}'. Must be one of: ${JSON.stringify([...VALID_POLICIES].sort())}`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
this.trustPolicy = policy;
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// src/sdk/key-manager.ts
|
|
762
|
+
var import_node_fs = require("fs");
|
|
763
|
+
var import_node_fs2 = require("fs");
|
|
764
|
+
var import_node_path2 = require("path");
|
|
765
|
+
var import_node_os2 = require("os");
|
|
766
|
+
var import_node_os3 = require("os");
|
|
767
|
+
var DEFAULT_KEY_DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".uam", "keys");
|
|
768
|
+
var KeyManager = class {
|
|
769
|
+
_keyDir;
|
|
770
|
+
_keypair = null;
|
|
771
|
+
constructor(keyDir) {
|
|
772
|
+
this._keyDir = keyDir ?? DEFAULT_KEY_DIR;
|
|
773
|
+
}
|
|
774
|
+
/** The 64-byte Ed25519 signing key. Throws if not loaded. */
|
|
775
|
+
get signingKey() {
|
|
776
|
+
if (this._keypair === null) {
|
|
777
|
+
throw new Error("No keypair loaded. Call loadOrGenerate() first.");
|
|
778
|
+
}
|
|
779
|
+
return this._keypair.signingKey;
|
|
780
|
+
}
|
|
781
|
+
/** The 32-byte Ed25519 verify (public) key. Throws if not loaded. */
|
|
782
|
+
get verifyKey() {
|
|
783
|
+
if (this._keypair === null) {
|
|
784
|
+
throw new Error("No keypair loaded. Call loadOrGenerate() first.");
|
|
785
|
+
}
|
|
786
|
+
return this._keypair.verifyKey;
|
|
787
|
+
}
|
|
788
|
+
/** The 32-byte seed. Throws if not loaded. */
|
|
789
|
+
get seed() {
|
|
790
|
+
if (this._keypair === null) {
|
|
791
|
+
throw new Error("No keypair loaded. Call loadOrGenerate() first.");
|
|
792
|
+
}
|
|
793
|
+
return this._keypair.seed;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Load existing keypair or generate a new one.
|
|
797
|
+
*
|
|
798
|
+
* Checks UAM_SIGNING_KEY env var first (base64-encoded Ed25519 seed).
|
|
799
|
+
* Falls back to file-based storage at {keyDir}/{name}.key.
|
|
800
|
+
* Generates a new keypair if neither source exists.
|
|
801
|
+
*/
|
|
802
|
+
loadOrGenerate(name) {
|
|
803
|
+
const envKey = process.env["UAM_SIGNING_KEY"];
|
|
804
|
+
if (envKey) {
|
|
805
|
+
this._keypair = deserializeSigningKey(envKey.trim());
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
(0, import_node_fs.mkdirSync)(this._keyDir, { recursive: true });
|
|
809
|
+
const keyPath = (0, import_node_path2.join)(this._keyDir, `${name}.key`);
|
|
810
|
+
const pubPath = (0, import_node_path2.join)(this._keyDir, `${name}.pub`);
|
|
811
|
+
if ((0, import_node_fs2.existsSync)(keyPath)) {
|
|
812
|
+
this._checkPermissions(keyPath);
|
|
813
|
+
const seedB64 = (0, import_node_fs.readFileSync)(keyPath, "utf-8").trim();
|
|
814
|
+
this._keypair = deserializeSigningKey(seedB64);
|
|
815
|
+
} else {
|
|
816
|
+
this._keypair = generateKeypair();
|
|
817
|
+
(0, import_node_fs.writeFileSync)(keyPath, serializeSigningKey(this._keypair.seed));
|
|
818
|
+
(0, import_node_fs.writeFileSync)(pubPath, serializeVerifyKey(this._keypair.verifyKey));
|
|
819
|
+
this._setPermissions(keyPath);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Store the relay token alongside the keypair.
|
|
824
|
+
*/
|
|
825
|
+
saveToken(name, token) {
|
|
826
|
+
(0, import_node_fs.mkdirSync)(this._keyDir, { recursive: true });
|
|
827
|
+
const tokenPath = (0, import_node_path2.join)(this._keyDir, `${name}.token`);
|
|
828
|
+
(0, import_node_fs.writeFileSync)(tokenPath, token);
|
|
829
|
+
this._setPermissions(tokenPath);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Load a previously saved token, or return null.
|
|
833
|
+
*
|
|
834
|
+
* Checks UAM_TOKEN env var first, then file-based storage,
|
|
835
|
+
* then legacy .api_key files for backward compatibility.
|
|
836
|
+
*/
|
|
837
|
+
loadToken(name) {
|
|
838
|
+
const envToken = process.env["UAM_TOKEN"];
|
|
839
|
+
if (envToken) {
|
|
840
|
+
return envToken.trim();
|
|
841
|
+
}
|
|
842
|
+
const tokenPath = (0, import_node_path2.join)(this._keyDir, `${name}.token`);
|
|
843
|
+
if ((0, import_node_fs2.existsSync)(tokenPath)) {
|
|
844
|
+
return (0, import_node_fs.readFileSync)(tokenPath, "utf-8").trim();
|
|
845
|
+
}
|
|
846
|
+
const legacyPath = (0, import_node_path2.join)(this._keyDir, `${name}.api_key`);
|
|
847
|
+
if ((0, import_node_fs2.existsSync)(legacyPath)) {
|
|
848
|
+
return (0, import_node_fs.readFileSync)(legacyPath, "utf-8").trim();
|
|
849
|
+
}
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
/** Set file permissions to 0o600 (owner read/write only). */
|
|
853
|
+
_setPermissions(path) {
|
|
854
|
+
if ((0, import_node_os3.platform)() !== "win32") {
|
|
855
|
+
(0, import_node_fs.chmodSync)(path, 384);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/** Warn if key file permissions are too permissive. */
|
|
859
|
+
_checkPermissions(path) {
|
|
860
|
+
if ((0, import_node_os3.platform)() === "win32") {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
const mode = (0, import_node_fs.statSync)(path).mode & 511;
|
|
865
|
+
if (mode !== 384) {
|
|
866
|
+
console.warn(
|
|
867
|
+
`Key file ${path} has permissions ${mode.toString(8)} (expected 600). Run: chmod 600 ${path}`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// src/sdk/contact-book.ts
|
|
876
|
+
var import_node_fs3 = require("fs");
|
|
877
|
+
var import_node_path3 = require("path");
|
|
878
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
879
|
+
var ContactBook = class {
|
|
880
|
+
_dbPath;
|
|
881
|
+
_db = null;
|
|
882
|
+
_knownAddresses = /* @__PURE__ */ new Set();
|
|
883
|
+
_blockedExact = /* @__PURE__ */ new Set();
|
|
884
|
+
_blockedDomains = /* @__PURE__ */ new Set();
|
|
885
|
+
constructor(dataDir) {
|
|
886
|
+
this._dbPath = (0, import_node_path3.join)(dataDir, "contacts", "contacts.db");
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Open the database, create tables, run migrations, load caches.
|
|
890
|
+
*/
|
|
891
|
+
open() {
|
|
892
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(this._dbPath), { recursive: true });
|
|
893
|
+
this._db = new import_better_sqlite3.default(this._dbPath);
|
|
894
|
+
this._db.pragma("journal_mode = WAL");
|
|
895
|
+
this._db.exec(`
|
|
896
|
+
CREATE TABLE IF NOT EXISTS contacts (
|
|
897
|
+
address TEXT PRIMARY KEY,
|
|
898
|
+
public_key TEXT NOT NULL,
|
|
899
|
+
display_name TEXT,
|
|
900
|
+
trust_state TEXT NOT NULL DEFAULT 'unknown',
|
|
901
|
+
trust_source TEXT DEFAULT 'legacy-unknown',
|
|
902
|
+
relay TEXT,
|
|
903
|
+
relays_json TEXT,
|
|
904
|
+
pinned_at TEXT,
|
|
905
|
+
first_seen TEXT NOT NULL DEFAULT (datetime('now')),
|
|
906
|
+
last_seen TEXT NOT NULL DEFAULT (datetime('now'))
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
CREATE TABLE IF NOT EXISTS pending_handshakes (
|
|
910
|
+
address TEXT PRIMARY KEY,
|
|
911
|
+
contact_card TEXT NOT NULL,
|
|
912
|
+
received_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
CREATE TABLE IF NOT EXISTS blocked_patterns (
|
|
916
|
+
pattern TEXT PRIMARY KEY,
|
|
917
|
+
blocked_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
918
|
+
);
|
|
919
|
+
`);
|
|
920
|
+
this._migrate();
|
|
921
|
+
const rows = this._db.prepare("SELECT address FROM contacts").all();
|
|
922
|
+
this._knownAddresses = new Set(rows.map((r) => r.address));
|
|
923
|
+
this._blockedExact.clear();
|
|
924
|
+
this._blockedDomains.clear();
|
|
925
|
+
const blocked = this._db.prepare("SELECT pattern FROM blocked_patterns").all();
|
|
926
|
+
for (const row of blocked) {
|
|
927
|
+
this._cacheBlockPattern(row.pattern);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Run schema migrations using PRAGMA user_version.
|
|
932
|
+
*/
|
|
933
|
+
_migrate() {
|
|
934
|
+
if (!this._db) return;
|
|
935
|
+
const versionRow = this._db.prepare("PRAGMA user_version").get();
|
|
936
|
+
const version = versionRow.user_version;
|
|
937
|
+
if (version < 2) {
|
|
938
|
+
this._db.exec("PRAGMA user_version = 3");
|
|
939
|
+
} else if (version < 3) {
|
|
940
|
+
this._db.exec(
|
|
941
|
+
"ALTER TABLE contacts ADD COLUMN pinned_at TEXT"
|
|
942
|
+
);
|
|
943
|
+
this._db.exec("PRAGMA user_version = 3");
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Close the database connection.
|
|
948
|
+
*/
|
|
949
|
+
close() {
|
|
950
|
+
if (this._db !== null) {
|
|
951
|
+
this._db.close();
|
|
952
|
+
this._db = null;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Check if an address is in the contact book (in-memory, no I/O).
|
|
957
|
+
*/
|
|
958
|
+
isKnown(address) {
|
|
959
|
+
return this._knownAddresses.has(address);
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Check if an address matches any blocked pattern (O(1) set lookup).
|
|
963
|
+
*/
|
|
964
|
+
isBlocked(address) {
|
|
965
|
+
if (this._blockedExact.has(address)) {
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
if (address.includes("::")) {
|
|
969
|
+
const domain = address.split("::")[1];
|
|
970
|
+
return this._blockedDomains.has(domain);
|
|
971
|
+
}
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Look up the public key for a known contact.
|
|
976
|
+
*/
|
|
977
|
+
getPublicKey(address) {
|
|
978
|
+
if (this._db === null) return null;
|
|
979
|
+
const row = this._db.prepare("SELECT public_key FROM contacts WHERE address = ?").get(address);
|
|
980
|
+
return row?.public_key ?? null;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Return the relay URLs for a known contact (CARD-04).
|
|
984
|
+
*/
|
|
985
|
+
getRelayUrls(address) {
|
|
986
|
+
if (this._db === null) return null;
|
|
987
|
+
const row = this._db.prepare("SELECT relay, relays_json FROM contacts WHERE address = ?").get(address);
|
|
988
|
+
if (!row) return null;
|
|
989
|
+
if (row.relays_json !== null) {
|
|
990
|
+
return JSON.parse(row.relays_json);
|
|
991
|
+
}
|
|
992
|
+
if (row.relay !== null) {
|
|
993
|
+
return [row.relay];
|
|
994
|
+
}
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Add or update a contact (upsert).
|
|
999
|
+
*/
|
|
1000
|
+
addContact(address, publicKey, options) {
|
|
1001
|
+
if (this._db === null) {
|
|
1002
|
+
throw new Error("ContactBook not open. Call open() first.");
|
|
1003
|
+
}
|
|
1004
|
+
const displayName = options?.displayName ?? null;
|
|
1005
|
+
const trustState = options?.trustState ?? "trusted";
|
|
1006
|
+
const trustSource = options?.trustSource ?? null;
|
|
1007
|
+
const relay = options?.relay ?? null;
|
|
1008
|
+
const relaysJson = options?.relays !== void 0 && options?.relays !== null ? JSON.stringify(options.relays) : null;
|
|
1009
|
+
this._db.prepare(
|
|
1010
|
+
`INSERT INTO contacts (address, public_key, display_name, trust_state, trust_source, relay, relays_json)
|
|
1011
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1012
|
+
ON CONFLICT(address) DO UPDATE SET
|
|
1013
|
+
public_key = excluded.public_key,
|
|
1014
|
+
display_name = excluded.display_name,
|
|
1015
|
+
trust_state = excluded.trust_state,
|
|
1016
|
+
trust_source = COALESCE(excluded.trust_source, contacts.trust_source),
|
|
1017
|
+
relay = COALESCE(excluded.relay, contacts.relay),
|
|
1018
|
+
relays_json = COALESCE(excluded.relays_json, contacts.relays_json),
|
|
1019
|
+
last_seen = datetime('now')`
|
|
1020
|
+
).run(address, publicKey, displayName, trustState, trustSource, relay, relaysJson);
|
|
1021
|
+
this._knownAddresses.add(address);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Return all contacts sorted by last_seen DESC.
|
|
1025
|
+
*/
|
|
1026
|
+
listContacts() {
|
|
1027
|
+
if (this._db === null) return [];
|
|
1028
|
+
const rows = this._db.prepare(
|
|
1029
|
+
"SELECT address, display_name, trust_state, first_seen, last_seen FROM contacts ORDER BY last_seen DESC"
|
|
1030
|
+
).all();
|
|
1031
|
+
return rows.map((r) => ({
|
|
1032
|
+
address: r.address,
|
|
1033
|
+
displayName: r.display_name,
|
|
1034
|
+
trustState: r.trust_state,
|
|
1035
|
+
firstSeen: r.first_seen,
|
|
1036
|
+
lastSeen: r.last_seen
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Store a pending handshake request.
|
|
1041
|
+
*/
|
|
1042
|
+
addPending(address, contactCardJson) {
|
|
1043
|
+
if (this._db === null) {
|
|
1044
|
+
throw new Error("ContactBook not open. Call open() first.");
|
|
1045
|
+
}
|
|
1046
|
+
this._db.prepare(
|
|
1047
|
+
`INSERT OR REPLACE INTO pending_handshakes (address, contact_card) VALUES (?, ?)`
|
|
1048
|
+
).run(address, contactCardJson);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Retrieve all pending handshake requests.
|
|
1052
|
+
*/
|
|
1053
|
+
getPending() {
|
|
1054
|
+
if (this._db === null) return [];
|
|
1055
|
+
const rows = this._db.prepare(
|
|
1056
|
+
"SELECT address, contact_card, received_at FROM pending_handshakes"
|
|
1057
|
+
).all();
|
|
1058
|
+
return rows.map((r) => ({
|
|
1059
|
+
address: r.address,
|
|
1060
|
+
contactCard: r.contact_card,
|
|
1061
|
+
receivedAt: r.received_at
|
|
1062
|
+
}));
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Remove a pending handshake request.
|
|
1066
|
+
*/
|
|
1067
|
+
removePending(address) {
|
|
1068
|
+
if (this._db === null) return;
|
|
1069
|
+
this._db.prepare("DELETE FROM pending_handshakes WHERE address = ?").run(address);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Return pending handshakes older than N days.
|
|
1073
|
+
*/
|
|
1074
|
+
getExpiredPending(days = 7) {
|
|
1075
|
+
if (this._db === null) return [];
|
|
1076
|
+
const rows = this._db.prepare(
|
|
1077
|
+
`SELECT address, contact_card, received_at FROM pending_handshakes
|
|
1078
|
+
WHERE datetime(received_at, '+' || ? || ' days') < datetime('now')`
|
|
1079
|
+
).all(String(days));
|
|
1080
|
+
return rows.map((r) => ({
|
|
1081
|
+
address: r.address,
|
|
1082
|
+
contactCard: r.contact_card,
|
|
1083
|
+
receivedAt: r.received_at
|
|
1084
|
+
}));
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Return the trust_state for an address, or null if unknown.
|
|
1088
|
+
*/
|
|
1089
|
+
getTrustState(address) {
|
|
1090
|
+
if (this._db === null) return null;
|
|
1091
|
+
const row = this._db.prepare("SELECT trust_state FROM contacts WHERE address = ?").get(address);
|
|
1092
|
+
return row?.trust_state ?? null;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Set the pinned_at timestamp and trust_state='pinned' for a contact (TOFU).
|
|
1096
|
+
*/
|
|
1097
|
+
setPinnedAt(address) {
|
|
1098
|
+
if (this._db === null) {
|
|
1099
|
+
throw new Error("ContactBook not open. Call open() first.");
|
|
1100
|
+
}
|
|
1101
|
+
this._db.prepare(
|
|
1102
|
+
"UPDATE contacts SET pinned_at = datetime('now'), trust_state = 'pinned' WHERE address = ?"
|
|
1103
|
+
).run(address);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Remove a contact by address. Returns true if a contact was deleted.
|
|
1107
|
+
*/
|
|
1108
|
+
removeContact(address) {
|
|
1109
|
+
if (this._db === null) {
|
|
1110
|
+
throw new Error("ContactBook not open. Call open() first.");
|
|
1111
|
+
}
|
|
1112
|
+
const result = this._db.prepare("DELETE FROM contacts WHERE address = ?").run(address);
|
|
1113
|
+
this._knownAddresses.delete(address);
|
|
1114
|
+
return result.changes > 0;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Return true if the address has trust_state 'trusted', 'verified', or 'pinned'.
|
|
1118
|
+
*
|
|
1119
|
+
* Convenience helper for inbound message filtering (CARD-05, TOFU-02).
|
|
1120
|
+
*/
|
|
1121
|
+
isTrustedOrVerified(address) {
|
|
1122
|
+
const state = this.getTrustState(address);
|
|
1123
|
+
return state === "trusted" || state === "verified" || state === "pinned";
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Block an address or domain pattern (e.g., '*::evil.com').
|
|
1127
|
+
*/
|
|
1128
|
+
addBlock(pattern) {
|
|
1129
|
+
if (this._db === null) {
|
|
1130
|
+
throw new Error("ContactBook not open. Call open() first.");
|
|
1131
|
+
}
|
|
1132
|
+
this._db.prepare("INSERT OR IGNORE INTO blocked_patterns (pattern) VALUES (?)").run(pattern);
|
|
1133
|
+
this._cacheBlockPattern(pattern);
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Remove a block pattern.
|
|
1137
|
+
*/
|
|
1138
|
+
removeBlock(pattern) {
|
|
1139
|
+
if (this._db === null) {
|
|
1140
|
+
throw new Error("ContactBook not open. Call open() first.");
|
|
1141
|
+
}
|
|
1142
|
+
this._db.prepare("DELETE FROM blocked_patterns WHERE pattern = ?").run(pattern);
|
|
1143
|
+
this._uncacheBlockPattern(pattern);
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Return all blocked patterns with their timestamps.
|
|
1147
|
+
*/
|
|
1148
|
+
listBlocked() {
|
|
1149
|
+
if (this._db === null) return [];
|
|
1150
|
+
const rows = this._db.prepare(
|
|
1151
|
+
"SELECT pattern, blocked_at FROM blocked_patterns ORDER BY blocked_at DESC"
|
|
1152
|
+
).all();
|
|
1153
|
+
return rows.map((r) => ({
|
|
1154
|
+
pattern: r.pattern,
|
|
1155
|
+
blockedAt: r.blocked_at
|
|
1156
|
+
}));
|
|
1157
|
+
}
|
|
1158
|
+
// -- Internal block caching ---------------------------------------------------
|
|
1159
|
+
_cacheBlockPattern(pattern) {
|
|
1160
|
+
if (pattern.startsWith("*::")) {
|
|
1161
|
+
this._blockedDomains.add(pattern.slice(3));
|
|
1162
|
+
} else {
|
|
1163
|
+
this._blockedExact.add(pattern);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
_uncacheBlockPattern(pattern) {
|
|
1167
|
+
if (pattern.startsWith("*::")) {
|
|
1168
|
+
this._blockedDomains.delete(pattern.slice(3));
|
|
1169
|
+
} else {
|
|
1170
|
+
this._blockedExact.delete(pattern);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// src/sdk/handshake.ts
|
|
1176
|
+
var HandshakeManager = class {
|
|
1177
|
+
_contactBook;
|
|
1178
|
+
_trustPolicy;
|
|
1179
|
+
constructor(contactBook, trustPolicy) {
|
|
1180
|
+
this._contactBook = contactBook;
|
|
1181
|
+
this._trustPolicy = trustPolicy;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Create a handshake request envelope with embedded contact card.
|
|
1185
|
+
*
|
|
1186
|
+
* The payload is the JSON-serialized contact card, encrypted with
|
|
1187
|
+
* SealedBox (anonymous encryption) to the recipient.
|
|
1188
|
+
*
|
|
1189
|
+
* Returns wire-format dict ready to send via transport.
|
|
1190
|
+
*/
|
|
1191
|
+
async createHandshakeRequest(agent, toAddress, recipientVk) {
|
|
1192
|
+
const card = await createContactCard(
|
|
1193
|
+
agent.address,
|
|
1194
|
+
agent._config.displayName,
|
|
1195
|
+
agent._config.relayWsUrl,
|
|
1196
|
+
agent._keyManager.signingKey
|
|
1197
|
+
);
|
|
1198
|
+
const cardJson = JSON.stringify(contactCardToDict(card));
|
|
1199
|
+
const envelope = await createEnvelope(
|
|
1200
|
+
agent.address,
|
|
1201
|
+
toAddress,
|
|
1202
|
+
"handshake.request" /* HANDSHAKE_REQUEST */,
|
|
1203
|
+
new TextEncoder().encode(cardJson),
|
|
1204
|
+
agent._keyManager.signingKey,
|
|
1205
|
+
recipientVk
|
|
1206
|
+
);
|
|
1207
|
+
return toWireDict(envelope);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Handle an inbound handshake message.
|
|
1211
|
+
*
|
|
1212
|
+
* Returns null -- handshake messages are not user-visible.
|
|
1213
|
+
*/
|
|
1214
|
+
async handleInbound(agent, envelope, senderVk) {
|
|
1215
|
+
if (envelope.type === "handshake.request" /* HANDSHAKE_REQUEST */) {
|
|
1216
|
+
await this._handleRequest(agent, envelope, senderVk);
|
|
1217
|
+
} else if (envelope.type === "handshake.accept" /* HANDSHAKE_ACCEPT */) {
|
|
1218
|
+
await this._handleAccept(envelope, senderVk);
|
|
1219
|
+
} else if (envelope.type === "handshake.deny" /* HANDSHAKE_DENY */) {
|
|
1220
|
+
}
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Process a handshake.request: decrypt contact card, apply trust policy.
|
|
1225
|
+
*/
|
|
1226
|
+
async _handleRequest(agent, envelope, senderVk) {
|
|
1227
|
+
const plaintext = decryptPayloadAnonymous(
|
|
1228
|
+
envelope.payload,
|
|
1229
|
+
agent._keyManager.signingKey
|
|
1230
|
+
);
|
|
1231
|
+
const cardDict = JSON.parse(
|
|
1232
|
+
new TextDecoder().decode(plaintext)
|
|
1233
|
+
);
|
|
1234
|
+
const card = contactCardFromDict(cardDict);
|
|
1235
|
+
verifyContactCard(card);
|
|
1236
|
+
if (this._trustPolicy === "auto-accept") {
|
|
1237
|
+
this._contactBook.addContact(card.address, card.publicKey, {
|
|
1238
|
+
displayName: card.displayName,
|
|
1239
|
+
trustState: "provisional",
|
|
1240
|
+
trustSource: "auto-accepted-provisional"
|
|
1241
|
+
});
|
|
1242
|
+
await this._sendAccept(agent, envelope.fromAddress, senderVk);
|
|
1243
|
+
} else if (this._trustPolicy === "allowlist-only") {
|
|
1244
|
+
await this._sendDeny(agent, envelope.fromAddress, senderVk);
|
|
1245
|
+
} else {
|
|
1246
|
+
this._contactBook.addPending(
|
|
1247
|
+
envelope.fromAddress,
|
|
1248
|
+
JSON.stringify(cardDict)
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Process a handshake.accept: store the sender as pinned (TOFU).
|
|
1254
|
+
*/
|
|
1255
|
+
async _handleAccept(envelope, senderVk) {
|
|
1256
|
+
const senderPkStr = serializeVerifyKey(senderVk);
|
|
1257
|
+
this._contactBook.addContact(envelope.fromAddress, senderPkStr, {
|
|
1258
|
+
trustState: "pinned"
|
|
1259
|
+
});
|
|
1260
|
+
this._contactBook.setPinnedAt(envelope.fromAddress);
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Send a handshake.accept envelope back to the requester.
|
|
1264
|
+
*/
|
|
1265
|
+
async _sendAccept(agent, toAddress, recipientVk) {
|
|
1266
|
+
const card = await createContactCard(
|
|
1267
|
+
agent.address,
|
|
1268
|
+
agent._config.displayName,
|
|
1269
|
+
agent._config.relayWsUrl,
|
|
1270
|
+
agent._keyManager.signingKey
|
|
1271
|
+
);
|
|
1272
|
+
const acceptPayload = JSON.stringify({
|
|
1273
|
+
status: "accepted",
|
|
1274
|
+
contact_card: contactCardToDict(card)
|
|
1275
|
+
});
|
|
1276
|
+
const envelope = await createEnvelope(
|
|
1277
|
+
agent.address,
|
|
1278
|
+
toAddress,
|
|
1279
|
+
"handshake.accept" /* HANDSHAKE_ACCEPT */,
|
|
1280
|
+
new TextEncoder().encode(acceptPayload),
|
|
1281
|
+
agent._keyManager.signingKey,
|
|
1282
|
+
recipientVk
|
|
1283
|
+
);
|
|
1284
|
+
const wire = toWireDict(envelope);
|
|
1285
|
+
if (agent._transport) {
|
|
1286
|
+
await agent._transport.send(wire);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Send a handshake.deny envelope to the requester.
|
|
1291
|
+
*/
|
|
1292
|
+
async _sendDeny(agent, toAddress, recipientVk) {
|
|
1293
|
+
const denyPayload = JSON.stringify({
|
|
1294
|
+
status: "denied",
|
|
1295
|
+
reason: "allowlist-only"
|
|
1296
|
+
});
|
|
1297
|
+
const envelope = await createEnvelope(
|
|
1298
|
+
agent.address,
|
|
1299
|
+
toAddress,
|
|
1300
|
+
"handshake.deny" /* HANDSHAKE_DENY */,
|
|
1301
|
+
new TextEncoder().encode(denyPayload),
|
|
1302
|
+
agent._keyManager.signingKey,
|
|
1303
|
+
recipientVk
|
|
1304
|
+
);
|
|
1305
|
+
const wire = toWireDict(envelope);
|
|
1306
|
+
if (agent._transport) {
|
|
1307
|
+
await agent._transport.send(wire);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
// src/sdk/tier3.ts
|
|
1313
|
+
var import_viem = require("viem");
|
|
1314
|
+
var DEFAULT_RPC_URL = "https://sepolia.base.org";
|
|
1315
|
+
var DEFAULT_CHAIN_ID = 84532;
|
|
1316
|
+
var CACHE_TTL_MS = 3600 * 1e3;
|
|
1317
|
+
var REGISTRY_ABI = [
|
|
1318
|
+
{
|
|
1319
|
+
name: "resolve",
|
|
1320
|
+
type: "function",
|
|
1321
|
+
stateMutability: "view",
|
|
1322
|
+
inputs: [{ name: "name", type: "string" }],
|
|
1323
|
+
outputs: [
|
|
1324
|
+
{ name: "owner", type: "address" },
|
|
1325
|
+
{ name: "publicKey", type: "string" },
|
|
1326
|
+
{ name: "relayUrl", type: "string" },
|
|
1327
|
+
{ name: "expiry", type: "uint256" }
|
|
1328
|
+
]
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
name: "available",
|
|
1332
|
+
type: "function",
|
|
1333
|
+
stateMutability: "view",
|
|
1334
|
+
inputs: [{ name: "name", type: "string" }],
|
|
1335
|
+
outputs: [{ name: "", type: "bool" }]
|
|
1336
|
+
}
|
|
1337
|
+
];
|
|
1338
|
+
function buildChain(chainId, rpcUrl) {
|
|
1339
|
+
return (0, import_viem.defineChain)({
|
|
1340
|
+
id: chainId,
|
|
1341
|
+
name: `chain-${chainId}`,
|
|
1342
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
1343
|
+
rpcUrls: {
|
|
1344
|
+
default: { http: [rpcUrl] }
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
var Tier3Resolver = class extends AddressResolver {
|
|
1349
|
+
_contractAddress;
|
|
1350
|
+
_client;
|
|
1351
|
+
_cacheTtlMs;
|
|
1352
|
+
_cache = /* @__PURE__ */ new Map();
|
|
1353
|
+
constructor(config) {
|
|
1354
|
+
super();
|
|
1355
|
+
this._contractAddress = config.contractAddress;
|
|
1356
|
+
this._cacheTtlMs = config.cacheTtlMs ?? CACHE_TTL_MS;
|
|
1357
|
+
const rpcUrl = config.rpcUrl ?? DEFAULT_RPC_URL;
|
|
1358
|
+
const chainId = config.chainId ?? DEFAULT_CHAIN_ID;
|
|
1359
|
+
this._client = (0, import_viem.createPublicClient)({
|
|
1360
|
+
chain: buildChain(chainId, rpcUrl),
|
|
1361
|
+
transport: (0, import_viem.http)(rpcUrl)
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
async resolvePublicKey(address, _token, _relayUrl) {
|
|
1365
|
+
const parsed = parseAddress(address);
|
|
1366
|
+
const name = parsed.domain;
|
|
1367
|
+
const cached = this._cache.get(name);
|
|
1368
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
1369
|
+
return cached.publicKey;
|
|
1370
|
+
}
|
|
1371
|
+
try {
|
|
1372
|
+
const result = await this._client.readContract({
|
|
1373
|
+
address: this._contractAddress,
|
|
1374
|
+
abi: REGISTRY_ABI,
|
|
1375
|
+
functionName: "resolve",
|
|
1376
|
+
args: [name]
|
|
1377
|
+
});
|
|
1378
|
+
const [_owner, publicKey, _relayUrl2, _expiry] = result;
|
|
1379
|
+
if (!publicKey) {
|
|
1380
|
+
throw new UAMError(
|
|
1381
|
+
`Tier 3 name '${name}' has no public key registered`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
this._cache.set(name, {
|
|
1385
|
+
publicKey,
|
|
1386
|
+
expiresAt: Date.now() + this._cacheTtlMs
|
|
1387
|
+
});
|
|
1388
|
+
return publicKey;
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
if (err instanceof UAMError) throw err;
|
|
1391
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1392
|
+
if (msg.includes("NameNotFound") || msg.includes("revert")) {
|
|
1393
|
+
throw new UAMError(`Tier 3 name not found on-chain: ${name}`);
|
|
1394
|
+
}
|
|
1395
|
+
throw new UAMError(`Tier 3 resolution failed for ${name}: ${msg}`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Check if a name is available for registration.
|
|
1400
|
+
*/
|
|
1401
|
+
async isAvailable(name) {
|
|
1402
|
+
try {
|
|
1403
|
+
const result = await this._client.readContract({
|
|
1404
|
+
address: this._contractAddress,
|
|
1405
|
+
abi: REGISTRY_ABI,
|
|
1406
|
+
functionName: "available",
|
|
1407
|
+
args: [name]
|
|
1408
|
+
});
|
|
1409
|
+
return result;
|
|
1410
|
+
} catch {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Clear cache for a specific name or all names.
|
|
1416
|
+
*/
|
|
1417
|
+
invalidateCache(name) {
|
|
1418
|
+
if (name) {
|
|
1419
|
+
this._cache.delete(name);
|
|
1420
|
+
} else {
|
|
1421
|
+
this._cache.clear();
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
// src/sdk/resolver.ts
|
|
1427
|
+
init_dns_verifier();
|
|
1428
|
+
var AddressResolver = class {
|
|
1429
|
+
};
|
|
1430
|
+
var Tier1Resolver = class extends AddressResolver {
|
|
1431
|
+
async resolvePublicKey(address, _token, relayUrl) {
|
|
1432
|
+
const resp = await fetch(
|
|
1433
|
+
`${relayUrl}/api/v1/agents/${address}/public-key`
|
|
1434
|
+
);
|
|
1435
|
+
if (resp.status === 404) {
|
|
1436
|
+
throw new UAMError(`Agent not found: ${address}`);
|
|
1437
|
+
}
|
|
1438
|
+
if (!resp.ok) {
|
|
1439
|
+
throw new UAMError(
|
|
1440
|
+
`Resolver request failed: ${resp.status} ${resp.statusText}`
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
const data = await resp.json();
|
|
1444
|
+
return data["public_key"];
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
var Tier2Resolver = class extends AddressResolver {
|
|
1448
|
+
async resolvePublicKey(address, _token, _relayUrl) {
|
|
1449
|
+
const parsed = parseAddress(address);
|
|
1450
|
+
const txtRecords = await queryUamTxt(parsed.domain);
|
|
1451
|
+
for (const txt of txtRecords) {
|
|
1452
|
+
const tags = parseUamTxt(txt);
|
|
1453
|
+
if (tags["v"] === "uam1") {
|
|
1454
|
+
const key = extractPublicKey(tags);
|
|
1455
|
+
if (key) return key;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const httpsKey = await resolveKeyViaHttps(parsed.agent, parsed.domain);
|
|
1459
|
+
if (httpsKey) return httpsKey;
|
|
1460
|
+
throw new UAMError(
|
|
1461
|
+
`Could not resolve public key for ${address} via DNS or HTTPS`
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
var SmartResolver = class extends AddressResolver {
|
|
1466
|
+
_relayDomain;
|
|
1467
|
+
_tier1;
|
|
1468
|
+
_tier2;
|
|
1469
|
+
_tier3;
|
|
1470
|
+
constructor(relayDomain, tier3Config) {
|
|
1471
|
+
super();
|
|
1472
|
+
this._relayDomain = relayDomain;
|
|
1473
|
+
this._tier1 = new Tier1Resolver();
|
|
1474
|
+
this._tier2 = new Tier2Resolver();
|
|
1475
|
+
this._tier3 = tier3Config ? new Tier3Resolver(tier3Config) : null;
|
|
1476
|
+
}
|
|
1477
|
+
async resolvePublicKey(address, token, relayUrl) {
|
|
1478
|
+
const parsed = parseAddress(address);
|
|
1479
|
+
const domain = parsed.domain;
|
|
1480
|
+
if (domain === this._relayDomain) {
|
|
1481
|
+
return this._tier1.resolvePublicKey(address, token, relayUrl);
|
|
1482
|
+
}
|
|
1483
|
+
if (domain.includes(".")) {
|
|
1484
|
+
return this._tier2.resolvePublicKey(address, token, relayUrl);
|
|
1485
|
+
}
|
|
1486
|
+
if (!this._tier3) {
|
|
1487
|
+
throw new UAMError(
|
|
1488
|
+
`Tier 3 resolution requires contract config. Cannot resolve dot-free domain: '${domain}'`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
return this._tier3.resolvePublicKey(address, token, relayUrl);
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
// src/sdk/transport/base.ts
|
|
1496
|
+
var TransportBase = class {
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
// src/sdk/transport/http.ts
|
|
1500
|
+
var HTTPTransport = class extends TransportBase {
|
|
1501
|
+
_relayUrl;
|
|
1502
|
+
_token;
|
|
1503
|
+
_address;
|
|
1504
|
+
constructor(relayUrl, token, address) {
|
|
1505
|
+
super();
|
|
1506
|
+
this._relayUrl = relayUrl.replace(/\/+$/, "");
|
|
1507
|
+
this._token = token;
|
|
1508
|
+
this._address = address;
|
|
1509
|
+
}
|
|
1510
|
+
async connect() {
|
|
1511
|
+
}
|
|
1512
|
+
async disconnect() {
|
|
1513
|
+
}
|
|
1514
|
+
async send(envelope) {
|
|
1515
|
+
const resp = await fetch(`${this._relayUrl}/api/v1/send`, {
|
|
1516
|
+
method: "POST",
|
|
1517
|
+
headers: {
|
|
1518
|
+
"Content-Type": "application/json",
|
|
1519
|
+
Authorization: `Bearer ${this._token}`
|
|
1520
|
+
},
|
|
1521
|
+
body: JSON.stringify({ envelope })
|
|
1522
|
+
});
|
|
1523
|
+
if (!resp.ok) {
|
|
1524
|
+
throw new Error(
|
|
1525
|
+
`HTTP send failed: ${resp.status} ${resp.statusText}`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
async receive(limit = 50) {
|
|
1530
|
+
const url = new URL(
|
|
1531
|
+
`${this._relayUrl}/api/v1/inbox/${this._address}`
|
|
1532
|
+
);
|
|
1533
|
+
url.searchParams.set("limit", String(limit));
|
|
1534
|
+
const resp = await fetch(url.toString(), {
|
|
1535
|
+
headers: {
|
|
1536
|
+
Authorization: `Bearer ${this._token}`
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
if (!resp.ok) {
|
|
1540
|
+
throw new Error(
|
|
1541
|
+
`HTTP receive failed: ${resp.status} ${resp.statusText}`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
const data = await resp.json();
|
|
1545
|
+
return data["messages"] ?? [];
|
|
1546
|
+
}
|
|
1547
|
+
async listen(_callback) {
|
|
1548
|
+
throw new Error(
|
|
1549
|
+
"HTTP transport does not support real-time listening. Use receive() for polling or switch to WebSocket transport."
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
|
|
1554
|
+
// src/sdk/transport/websocket.ts
|
|
1555
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
1556
|
+
var BASE_DELAY = 1;
|
|
1557
|
+
var MAX_DELAY = 60;
|
|
1558
|
+
var JITTER_RANGE = 1;
|
|
1559
|
+
var WebSocketTransport = class extends TransportBase {
|
|
1560
|
+
_url;
|
|
1561
|
+
_onMessage;
|
|
1562
|
+
_ws = null;
|
|
1563
|
+
_pending = [];
|
|
1564
|
+
_connected = false;
|
|
1565
|
+
_connectResolve = null;
|
|
1566
|
+
_connectReject = null;
|
|
1567
|
+
_running = false;
|
|
1568
|
+
_reconnectTimeout = null;
|
|
1569
|
+
constructor(wsUrl, token, onMessage) {
|
|
1570
|
+
super();
|
|
1571
|
+
this._url = `${wsUrl}?token=${token}`;
|
|
1572
|
+
this._onMessage = onMessage ?? null;
|
|
1573
|
+
}
|
|
1574
|
+
async connect() {
|
|
1575
|
+
this._running = true;
|
|
1576
|
+
return new Promise((resolve, reject) => {
|
|
1577
|
+
this._connectResolve = resolve;
|
|
1578
|
+
this._connectReject = reject;
|
|
1579
|
+
this._startConnectionLoop();
|
|
1580
|
+
const timeout = setTimeout(() => {
|
|
1581
|
+
this._connectReject = null;
|
|
1582
|
+
this._connectResolve = null;
|
|
1583
|
+
reject(new Error("WebSocket connection timeout (30s)"));
|
|
1584
|
+
}, 3e4);
|
|
1585
|
+
const origResolve = this._connectResolve;
|
|
1586
|
+
this._connectResolve = () => {
|
|
1587
|
+
clearTimeout(timeout);
|
|
1588
|
+
origResolve?.();
|
|
1589
|
+
};
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
async disconnect() {
|
|
1593
|
+
this._running = false;
|
|
1594
|
+
if (this._reconnectTimeout !== null) {
|
|
1595
|
+
clearTimeout(this._reconnectTimeout);
|
|
1596
|
+
this._reconnectTimeout = null;
|
|
1597
|
+
}
|
|
1598
|
+
if (this._ws) {
|
|
1599
|
+
this._ws.removeAllListeners();
|
|
1600
|
+
this._ws.close();
|
|
1601
|
+
this._ws = null;
|
|
1602
|
+
}
|
|
1603
|
+
this._connected = false;
|
|
1604
|
+
}
|
|
1605
|
+
async send(envelope) {
|
|
1606
|
+
if (!this._ws || this._ws.readyState !== import_ws.default.OPEN) {
|
|
1607
|
+
throw new Error("WebSocket not connected");
|
|
1608
|
+
}
|
|
1609
|
+
this._ws.send(JSON.stringify(envelope));
|
|
1610
|
+
}
|
|
1611
|
+
async receive(limit = 50) {
|
|
1612
|
+
const result = this._pending.slice(0, limit);
|
|
1613
|
+
this._pending = this._pending.slice(limit);
|
|
1614
|
+
return result;
|
|
1615
|
+
}
|
|
1616
|
+
async listen(callback) {
|
|
1617
|
+
this._onMessage = callback;
|
|
1618
|
+
}
|
|
1619
|
+
_startConnectionLoop() {
|
|
1620
|
+
let attempt = 0;
|
|
1621
|
+
const tryConnect = () => {
|
|
1622
|
+
if (!this._running) return;
|
|
1623
|
+
const ws = new import_ws.default(this._url);
|
|
1624
|
+
this._ws = ws;
|
|
1625
|
+
ws.on("open", () => {
|
|
1626
|
+
this._connected = true;
|
|
1627
|
+
attempt = 0;
|
|
1628
|
+
if (this._connectResolve) {
|
|
1629
|
+
this._connectResolve();
|
|
1630
|
+
this._connectResolve = null;
|
|
1631
|
+
this._connectReject = null;
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
ws.on("message", (data) => {
|
|
1635
|
+
try {
|
|
1636
|
+
const msg = JSON.parse(data.toString());
|
|
1637
|
+
this._handleMessage(msg);
|
|
1638
|
+
} catch {
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
ws.on("close", () => {
|
|
1642
|
+
this._connected = false;
|
|
1643
|
+
this._ws = null;
|
|
1644
|
+
if (!this._running) return;
|
|
1645
|
+
attempt += 1;
|
|
1646
|
+
const delay = Math.min(BASE_DELAY * Math.pow(2, attempt), MAX_DELAY);
|
|
1647
|
+
const jitter = Math.random() * JITTER_RANGE;
|
|
1648
|
+
const totalDelay = (delay + jitter) * 1e3;
|
|
1649
|
+
this._reconnectTimeout = setTimeout(tryConnect, totalDelay);
|
|
1650
|
+
});
|
|
1651
|
+
ws.on("error", (err) => {
|
|
1652
|
+
if (this._connectReject && !this._connected) {
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
};
|
|
1656
|
+
tryConnect();
|
|
1657
|
+
}
|
|
1658
|
+
_handleMessage(msg) {
|
|
1659
|
+
const msgType = msg["type"];
|
|
1660
|
+
if (msgType === "ping") {
|
|
1661
|
+
if (this._ws && this._ws.readyState === import_ws.default.OPEN) {
|
|
1662
|
+
this._ws.send(JSON.stringify({ type: "pong" }));
|
|
1663
|
+
}
|
|
1664
|
+
} else if (msgType === "ack") {
|
|
1665
|
+
} else if (msgType === "error" || "error" in msg) {
|
|
1666
|
+
} else if ("uam_version" in msg) {
|
|
1667
|
+
if (this._onMessage) {
|
|
1668
|
+
this._onMessage(msg).catch(() => {
|
|
1669
|
+
});
|
|
1670
|
+
} else {
|
|
1671
|
+
this._pending.push(msg);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
// src/sdk/transport/index.ts
|
|
1678
|
+
function createTransport(config, token, address, onMessage) {
|
|
1679
|
+
if (config.transportType === "http") {
|
|
1680
|
+
return new HTTPTransport(config.relayUrl, token, address);
|
|
1681
|
+
} else {
|
|
1682
|
+
return new WebSocketTransport(config.relayWsUrl, token, onMessage);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// src/sdk/agent.ts
|
|
1687
|
+
var Agent = class {
|
|
1688
|
+
/** @internal */
|
|
1689
|
+
_config;
|
|
1690
|
+
/** @internal */
|
|
1691
|
+
_keyManager;
|
|
1692
|
+
/** @internal */
|
|
1693
|
+
_transport = null;
|
|
1694
|
+
_resolver;
|
|
1695
|
+
_address = null;
|
|
1696
|
+
_token = null;
|
|
1697
|
+
_connected = false;
|
|
1698
|
+
_autoRegister;
|
|
1699
|
+
/** @internal */
|
|
1700
|
+
_contactBook;
|
|
1701
|
+
/** @internal */
|
|
1702
|
+
_handshake;
|
|
1703
|
+
/**
|
|
1704
|
+
* Create an Agent. No I/O happens here -- call connect() to initialize.
|
|
1705
|
+
*/
|
|
1706
|
+
constructor(name, options) {
|
|
1707
|
+
this._config = new SDKConfig({
|
|
1708
|
+
name,
|
|
1709
|
+
relayUrl: options?.relay,
|
|
1710
|
+
relayDomain: options?.domain ?? void 0,
|
|
1711
|
+
keyDir: options?.keyDir,
|
|
1712
|
+
displayName: options?.displayName ?? name,
|
|
1713
|
+
transportType: options?.transport ?? "websocket",
|
|
1714
|
+
trustPolicy: options?.trustPolicy ?? "auto-accept"
|
|
1715
|
+
});
|
|
1716
|
+
this._keyManager = new KeyManager(this._config.keyDir);
|
|
1717
|
+
this._resolver = new SmartResolver(this._config.relayDomain);
|
|
1718
|
+
this._autoRegister = options?.autoRegister ?? true;
|
|
1719
|
+
this._contactBook = new ContactBook(this._config.dataDir);
|
|
1720
|
+
this._handshake = new HandshakeManager(
|
|
1721
|
+
this._contactBook,
|
|
1722
|
+
this._config.trustPolicy
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
// -- Properties ----------------------------------------------------------
|
|
1726
|
+
/** The agent's full UAM address (e.g., 'myagent::youam.network'). */
|
|
1727
|
+
get address() {
|
|
1728
|
+
if (this._address === null) {
|
|
1729
|
+
throw new Error(
|
|
1730
|
+
"Agent not yet connected. Call await agent.connect() first."
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
return this._address;
|
|
1734
|
+
}
|
|
1735
|
+
/** UAM SDK version. */
|
|
1736
|
+
get version() {
|
|
1737
|
+
return "0.1.0";
|
|
1738
|
+
}
|
|
1739
|
+
/** The agent's public key (base64-encoded Ed25519 verify key). */
|
|
1740
|
+
get publicKey() {
|
|
1741
|
+
return serializeVerifyKey(this._keyManager.verifyKey);
|
|
1742
|
+
}
|
|
1743
|
+
/** Whether the agent has completed connection setup. */
|
|
1744
|
+
get isConnected() {
|
|
1745
|
+
return this._connected;
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Generate and return a signed contact card for this agent.
|
|
1749
|
+
*/
|
|
1750
|
+
async contactCard() {
|
|
1751
|
+
if (!this._connected) {
|
|
1752
|
+
throw new Error("Agent not connected. Call connect() first.");
|
|
1753
|
+
}
|
|
1754
|
+
const card = await createContactCard(
|
|
1755
|
+
this._address,
|
|
1756
|
+
this._config.displayName,
|
|
1757
|
+
this._config.relayWsUrl,
|
|
1758
|
+
this._keyManager.signingKey
|
|
1759
|
+
);
|
|
1760
|
+
return contactCardToDict(card);
|
|
1761
|
+
}
|
|
1762
|
+
// -- Lifecycle -----------------------------------------------------------
|
|
1763
|
+
/**
|
|
1764
|
+
* Initialize the agent: load/generate keys, register, connect transport.
|
|
1765
|
+
*
|
|
1766
|
+
* Idempotent -- calling twice is safe.
|
|
1767
|
+
*/
|
|
1768
|
+
async connect() {
|
|
1769
|
+
if (this._connected) {
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
await sodiumReady;
|
|
1773
|
+
this._keyManager.loadOrGenerate(this._config.name);
|
|
1774
|
+
const storedToken = this._keyManager.loadToken(this._config.name);
|
|
1775
|
+
if (storedToken) {
|
|
1776
|
+
this._token = storedToken;
|
|
1777
|
+
this._address = `${this._config.name}::${this._config.relayDomain}`;
|
|
1778
|
+
} else if (this._autoRegister) {
|
|
1779
|
+
await this._registerWithRelay();
|
|
1780
|
+
} else {
|
|
1781
|
+
throw new UAMError(
|
|
1782
|
+
"No stored token and autoRegister=false. Register manually or set autoRegister=true."
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
this._transport = createTransport(
|
|
1786
|
+
this._config,
|
|
1787
|
+
this._token,
|
|
1788
|
+
this._address
|
|
1789
|
+
);
|
|
1790
|
+
await this._transport.connect();
|
|
1791
|
+
this._contactBook.open();
|
|
1792
|
+
this._connected = true;
|
|
1793
|
+
await this._sweepExpiredHandshakes();
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Disconnect the transport and clean up resources.
|
|
1797
|
+
*/
|
|
1798
|
+
async close() {
|
|
1799
|
+
this._contactBook.close();
|
|
1800
|
+
if (this._transport) {
|
|
1801
|
+
await this._transport.disconnect();
|
|
1802
|
+
}
|
|
1803
|
+
this._connected = false;
|
|
1804
|
+
}
|
|
1805
|
+
// -- Messaging -----------------------------------------------------------
|
|
1806
|
+
/**
|
|
1807
|
+
* Send an encrypted, signed message. Returns the message_id.
|
|
1808
|
+
*/
|
|
1809
|
+
async send(toAddress, message, options) {
|
|
1810
|
+
await this._ensureConnected();
|
|
1811
|
+
if (this._config.trustPolicy === "require_verify") {
|
|
1812
|
+
const trust = this._contactBook.getTrustState(toAddress);
|
|
1813
|
+
if (trust !== "pinned" && trust !== "verified") {
|
|
1814
|
+
throw new UAMError(
|
|
1815
|
+
`Trust policy 'require_verify' requires fingerprint verification before sending to ${toAddress}. Run: uam contact verify ${toAddress}`
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const recipientVk = await this._resolvePublicKey(toAddress);
|
|
1820
|
+
if (!this._contactBook.isKnown(toAddress)) {
|
|
1821
|
+
await this._initiateHandshake(toAddress, recipientVk);
|
|
1822
|
+
}
|
|
1823
|
+
const envelope = await createEnvelope(
|
|
1824
|
+
this._address,
|
|
1825
|
+
toAddress,
|
|
1826
|
+
"message" /* MESSAGE */,
|
|
1827
|
+
new TextEncoder().encode(message),
|
|
1828
|
+
this._keyManager.signingKey,
|
|
1829
|
+
recipientVk,
|
|
1830
|
+
{
|
|
1831
|
+
threadId: options?.threadId,
|
|
1832
|
+
mediaType: "text/plain",
|
|
1833
|
+
attachments: options?.attachments
|
|
1834
|
+
}
|
|
1835
|
+
);
|
|
1836
|
+
const wire = toWireDict(envelope);
|
|
1837
|
+
const relayUrls = await this._getRelayUrls(toAddress);
|
|
1838
|
+
if (relayUrls !== null && relayUrls.length === 1 && relayUrls[0] === this._config.relayUrl) {
|
|
1839
|
+
await this._transport.send(wire);
|
|
1840
|
+
} else if (relayUrls !== null && relayUrls.length > 0) {
|
|
1841
|
+
await this._trySendWithFailover(wire, relayUrls);
|
|
1842
|
+
} else {
|
|
1843
|
+
await this._transport.send(wire);
|
|
1844
|
+
}
|
|
1845
|
+
return envelope.messageId;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Retrieve, decrypt, and verify pending messages.
|
|
1849
|
+
*
|
|
1850
|
+
* Returns a list of ReceivedMessage data objects.
|
|
1851
|
+
* Silently drops messages with invalid signatures or decryption errors.
|
|
1852
|
+
*/
|
|
1853
|
+
async inbox(limit = 50) {
|
|
1854
|
+
await this._ensureConnected();
|
|
1855
|
+
await this._sweepExpiredHandshakes();
|
|
1856
|
+
const rawMessages = await this._transport.receive(limit);
|
|
1857
|
+
const result = [];
|
|
1858
|
+
for (const raw of rawMessages) {
|
|
1859
|
+
const msg = await this._processInbound(raw);
|
|
1860
|
+
if (msg !== null) {
|
|
1861
|
+
result.push(msg);
|
|
1862
|
+
await this._sendReadReceipt(msg);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return result;
|
|
1866
|
+
}
|
|
1867
|
+
// -- Trust management (HAND-02, HAND-04) ---------------------------------
|
|
1868
|
+
/** List pending handshake requests. */
|
|
1869
|
+
async pending() {
|
|
1870
|
+
await this._ensureConnected();
|
|
1871
|
+
return this._contactBook.getPending();
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Approve a pending handshake request.
|
|
1875
|
+
*
|
|
1876
|
+
* Adds the sender to contacts with trust_source='explicit-approval'
|
|
1877
|
+
* and sends handshake.accept back.
|
|
1878
|
+
*/
|
|
1879
|
+
async approve(address) {
|
|
1880
|
+
await this._ensureConnected();
|
|
1881
|
+
const pendingList = this._contactBook.getPending();
|
|
1882
|
+
const entry = pendingList.find((p) => p.address === address);
|
|
1883
|
+
if (!entry) {
|
|
1884
|
+
throw new UAMError(`No pending handshake from ${address}`);
|
|
1885
|
+
}
|
|
1886
|
+
const cardDict = JSON.parse(entry.contactCard);
|
|
1887
|
+
const card = contactCardFromDict(cardDict);
|
|
1888
|
+
verifyContactCard(card);
|
|
1889
|
+
this._contactBook.addContact(card.address, card.publicKey, {
|
|
1890
|
+
displayName: card.displayName,
|
|
1891
|
+
trustState: "trusted",
|
|
1892
|
+
trustSource: "explicit-approval"
|
|
1893
|
+
});
|
|
1894
|
+
this._contactBook.removePending(address);
|
|
1895
|
+
const senderVk = deserializeVerifyKey(card.publicKey);
|
|
1896
|
+
await this._handshake._sendAccept(this, address, senderVk);
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Deny a pending handshake request.
|
|
1900
|
+
*
|
|
1901
|
+
* Removes the entry from pending and sends handshake.deny.
|
|
1902
|
+
*/
|
|
1903
|
+
async deny(address) {
|
|
1904
|
+
await this._ensureConnected();
|
|
1905
|
+
const pendingList = this._contactBook.getPending();
|
|
1906
|
+
const entry = pendingList.find((p) => p.address === address);
|
|
1907
|
+
if (!entry) {
|
|
1908
|
+
throw new UAMError(`No pending handshake from ${address}`);
|
|
1909
|
+
}
|
|
1910
|
+
const cardDict = JSON.parse(entry.contactCard);
|
|
1911
|
+
const card = contactCardFromDict(cardDict);
|
|
1912
|
+
const senderVk = deserializeVerifyKey(card.publicKey);
|
|
1913
|
+
this._contactBook.removePending(address);
|
|
1914
|
+
await this._handshake._sendDeny(this, address, senderVk);
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Block an address or domain pattern (HAND-04).
|
|
1918
|
+
*/
|
|
1919
|
+
async block(pattern) {
|
|
1920
|
+
await this._ensureConnected();
|
|
1921
|
+
this._contactBook.addBlock(pattern);
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Remove a block pattern (HAND-04).
|
|
1925
|
+
*/
|
|
1926
|
+
async unblock(pattern) {
|
|
1927
|
+
await this._ensureConnected();
|
|
1928
|
+
this._contactBook.removeBlock(pattern);
|
|
1929
|
+
}
|
|
1930
|
+
// -- Domain verification (DNS-02) ----------------------------------------
|
|
1931
|
+
/**
|
|
1932
|
+
* Verify domain ownership via relay endpoint (DNS-02).
|
|
1933
|
+
*
|
|
1934
|
+
* Polls `POST /api/v1/verify-domain` until the relay confirms
|
|
1935
|
+
* DNS TXT (or HTTPS fallback) verification succeeds, or timeout
|
|
1936
|
+
* elapses.
|
|
1937
|
+
*
|
|
1938
|
+
* Returns `true` if verified, `false` on timeout.
|
|
1939
|
+
*/
|
|
1940
|
+
async verifyDomain(domain, options) {
|
|
1941
|
+
const { generateTxtRecord: generateTxtRecord2 } = await Promise.resolve().then(() => (init_dns_verifier(), dns_verifier_exports));
|
|
1942
|
+
await this._ensureConnected();
|
|
1943
|
+
const timeout = options?.timeout ?? 3e5;
|
|
1944
|
+
const pollInterval = options?.pollInterval ?? 1e4;
|
|
1945
|
+
const expectedTxt = generateTxtRecord2(
|
|
1946
|
+
this.publicKey,
|
|
1947
|
+
this._config.relayUrl
|
|
1948
|
+
);
|
|
1949
|
+
void expectedTxt;
|
|
1950
|
+
const start = Date.now();
|
|
1951
|
+
while (Date.now() - start < timeout) {
|
|
1952
|
+
try {
|
|
1953
|
+
const resp = await fetch(
|
|
1954
|
+
`${this._config.relayUrl}/api/v1/verify-domain`,
|
|
1955
|
+
{
|
|
1956
|
+
method: "POST",
|
|
1957
|
+
headers: {
|
|
1958
|
+
"Content-Type": "application/json",
|
|
1959
|
+
Authorization: `Bearer ${this._token}`
|
|
1960
|
+
},
|
|
1961
|
+
body: JSON.stringify({ domain }),
|
|
1962
|
+
signal: AbortSignal.timeout(3e4)
|
|
1963
|
+
}
|
|
1964
|
+
);
|
|
1965
|
+
if (resp.ok) {
|
|
1966
|
+
const result = await resp.json();
|
|
1967
|
+
if (result["status"] === "verified") {
|
|
1968
|
+
return true;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
} catch {
|
|
1972
|
+
}
|
|
1973
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
1974
|
+
}
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
// -- Internal methods ----------------------------------------------------
|
|
1978
|
+
/** Multi-relay failover (CARD-06). */
|
|
1979
|
+
async _getRelayUrls(toAddress) {
|
|
1980
|
+
const urls = this._contactBook.getRelayUrls(toAddress);
|
|
1981
|
+
if (urls !== null) {
|
|
1982
|
+
return urls;
|
|
1983
|
+
}
|
|
1984
|
+
return [this._config.relayUrl];
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Try sending wire envelope to each relay URL in order (CARD-06).
|
|
1988
|
+
*
|
|
1989
|
+
* Uses transient fetch POST requests so the envelope can be delivered
|
|
1990
|
+
* to any relay that hosts the recipient.
|
|
1991
|
+
*/
|
|
1992
|
+
async _trySendWithFailover(wire, relayUrls) {
|
|
1993
|
+
let lastError = null;
|
|
1994
|
+
for (const url of relayUrls) {
|
|
1995
|
+
let base = url.replace(/\/+$/, "");
|
|
1996
|
+
if (base.endsWith("/ws")) {
|
|
1997
|
+
base = base.slice(0, -3);
|
|
1998
|
+
}
|
|
1999
|
+
base = base.replace("wss://", "https://").replace("ws://", "http://");
|
|
2000
|
+
const sendUrl = `${base}/api/v1/send`;
|
|
2001
|
+
try {
|
|
2002
|
+
const resp = await fetch(sendUrl, {
|
|
2003
|
+
method: "POST",
|
|
2004
|
+
headers: {
|
|
2005
|
+
"Content-Type": "application/json",
|
|
2006
|
+
Authorization: `Bearer ${this._token}`
|
|
2007
|
+
},
|
|
2008
|
+
body: JSON.stringify({ envelope: wire }),
|
|
2009
|
+
signal: AbortSignal.timeout(1e4)
|
|
2010
|
+
});
|
|
2011
|
+
if (!resp.ok) {
|
|
2012
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
2013
|
+
}
|
|
2014
|
+
return;
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (lastError !== null) {
|
|
2020
|
+
throw lastError;
|
|
2021
|
+
}
|
|
2022
|
+
throw new UAMError("No relay URLs to try");
|
|
2023
|
+
}
|
|
2024
|
+
async _ensureConnected() {
|
|
2025
|
+
if (!this._connected) {
|
|
2026
|
+
await this.connect();
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Resolve a recipient's public key, checking contact book first.
|
|
2031
|
+
*
|
|
2032
|
+
* TOFU-01: Known contacts always use stored key (zero network I/O).
|
|
2033
|
+
* TOFU-03: Race-condition double-check after network resolve.
|
|
2034
|
+
* Raises KeyPinningError if a pinned contact's key doesn't match.
|
|
2035
|
+
*/
|
|
2036
|
+
async _resolvePublicKey(toAddress) {
|
|
2037
|
+
const pkStr = this._contactBook.getPublicKey(toAddress);
|
|
2038
|
+
if (pkStr !== null) {
|
|
2039
|
+
return deserializeVerifyKey(pkStr);
|
|
2040
|
+
}
|
|
2041
|
+
const resolvedPkStr = await this._resolver.resolvePublicKey(
|
|
2042
|
+
toAddress,
|
|
2043
|
+
this._token,
|
|
2044
|
+
this._config.relayUrl
|
|
2045
|
+
);
|
|
2046
|
+
const storedPk = this._contactBook.getPublicKey(toAddress);
|
|
2047
|
+
if (storedPk !== null && storedPk !== resolvedPkStr) {
|
|
2048
|
+
const trust = this._contactBook.getTrustState(toAddress);
|
|
2049
|
+
if (trust === "pinned" || trust === "trusted" || trust === "verified") {
|
|
2050
|
+
throw new KeyPinningError(
|
|
2051
|
+
`CRITICAL: Public key mismatch for ${toAddress}! Stored key does not match resolved key. This may indicate a relay MITM attack. To accept the new key: uam contact remove ${toAddress}`
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
this._contactBook.addContact(toAddress, resolvedPkStr, {
|
|
2056
|
+
trustState: "provisional"
|
|
2057
|
+
});
|
|
2058
|
+
return deserializeVerifyKey(resolvedPkStr);
|
|
2059
|
+
}
|
|
2060
|
+
async _initiateHandshake(toAddress, recipientVk) {
|
|
2061
|
+
const wire = await this._handshake.createHandshakeRequest(
|
|
2062
|
+
this,
|
|
2063
|
+
toAddress,
|
|
2064
|
+
recipientVk
|
|
2065
|
+
);
|
|
2066
|
+
await this._transport.send(wire);
|
|
2067
|
+
this._contactBook.addContact(
|
|
2068
|
+
toAddress,
|
|
2069
|
+
serializeVerifyKey(recipientVk),
|
|
2070
|
+
{ trustState: "handshake-sent" }
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Process a single inbound envelope: verify, decrypt, handle handshakes.
|
|
2075
|
+
*
|
|
2076
|
+
* Returns null for invalid messages or handshake protocol messages.
|
|
2077
|
+
*/
|
|
2078
|
+
async _processInbound(raw) {
|
|
2079
|
+
const envelope = fromWireDict(raw);
|
|
2080
|
+
if (this._contactBook.isBlocked(envelope.fromAddress)) {
|
|
2081
|
+
return null;
|
|
2082
|
+
}
|
|
2083
|
+
let senderPkStr = this._contactBook.getPublicKey(envelope.fromAddress);
|
|
2084
|
+
if (senderPkStr === null) {
|
|
2085
|
+
try {
|
|
2086
|
+
senderPkStr = await this._resolver.resolvePublicKey(
|
|
2087
|
+
envelope.fromAddress,
|
|
2088
|
+
this._token,
|
|
2089
|
+
this._config.relayUrl
|
|
2090
|
+
);
|
|
2091
|
+
} catch {
|
|
2092
|
+
return null;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
const senderVk = deserializeVerifyKey(senderPkStr);
|
|
2096
|
+
try {
|
|
2097
|
+
verifyEnvelope(envelope, senderVk);
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
if (err instanceof SignatureVerificationError) {
|
|
2100
|
+
return null;
|
|
2101
|
+
}
|
|
2102
|
+
throw err;
|
|
2103
|
+
}
|
|
2104
|
+
if (envelope.type === "handshake.request" /* HANDSHAKE_REQUEST */ || envelope.type === "handshake.accept" /* HANDSHAKE_ACCEPT */ || envelope.type === "handshake.deny" /* HANDSHAKE_DENY */) {
|
|
2105
|
+
return this._handshake.handleInbound(this, envelope, senderVk);
|
|
2106
|
+
}
|
|
2107
|
+
if (this._handshake._trustPolicy !== "auto-accept") {
|
|
2108
|
+
if (!this._contactBook.isTrustedOrVerified(envelope.fromAddress)) {
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
let plaintextBytes;
|
|
2113
|
+
try {
|
|
2114
|
+
plaintextBytes = decryptPayload(
|
|
2115
|
+
envelope.payload,
|
|
2116
|
+
this._keyManager.signingKey,
|
|
2117
|
+
senderVk
|
|
2118
|
+
);
|
|
2119
|
+
} catch (err) {
|
|
2120
|
+
if (err instanceof DecryptionError) {
|
|
2121
|
+
return null;
|
|
2122
|
+
}
|
|
2123
|
+
throw err;
|
|
2124
|
+
}
|
|
2125
|
+
return Object.freeze({
|
|
2126
|
+
messageId: envelope.messageId,
|
|
2127
|
+
fromAddress: envelope.fromAddress,
|
|
2128
|
+
toAddress: envelope.toAddress,
|
|
2129
|
+
content: new TextDecoder().decode(plaintextBytes),
|
|
2130
|
+
timestamp: envelope.timestamp,
|
|
2131
|
+
type: envelope.type,
|
|
2132
|
+
threadId: envelope.threadId,
|
|
2133
|
+
replyTo: envelope.replyTo,
|
|
2134
|
+
mediaType: envelope.mediaType,
|
|
2135
|
+
verified: true
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Sweep expired pending handshakes and send receipt.failed (HAND-03).
|
|
2140
|
+
*/
|
|
2141
|
+
async _sweepExpiredHandshakes() {
|
|
2142
|
+
const expired = this._contactBook.getExpiredPending(7);
|
|
2143
|
+
for (const entry of expired) {
|
|
2144
|
+
try {
|
|
2145
|
+
const cardDict = JSON.parse(entry.contactCard);
|
|
2146
|
+
const card = contactCardFromDict(cardDict);
|
|
2147
|
+
const recipientVk = deserializeVerifyKey(card.publicKey);
|
|
2148
|
+
const failPayload = JSON.stringify({
|
|
2149
|
+
reason: "handshake_expired",
|
|
2150
|
+
original_from: entry.address
|
|
2151
|
+
});
|
|
2152
|
+
const envelope = await createEnvelope(
|
|
2153
|
+
this._address,
|
|
2154
|
+
entry.address,
|
|
2155
|
+
"receipt.failed" /* RECEIPT_FAILED */,
|
|
2156
|
+
new TextEncoder().encode(failPayload),
|
|
2157
|
+
this._keyManager.signingKey,
|
|
2158
|
+
recipientVk
|
|
2159
|
+
);
|
|
2160
|
+
const wire = toWireDict(envelope);
|
|
2161
|
+
await this._transport.send(wire);
|
|
2162
|
+
} catch {
|
|
2163
|
+
}
|
|
2164
|
+
this._contactBook.removePending(entry.address);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Send receipt.read back to the sender (RCPT-01).
|
|
2169
|
+
*
|
|
2170
|
+
* Fire-and-forget: errors are never propagated.
|
|
2171
|
+
* Anti-loop guard: receipts, handshakes, and session messages are skipped.
|
|
2172
|
+
*/
|
|
2173
|
+
async _sendReadReceipt(msg) {
|
|
2174
|
+
if (msg.type.startsWith("receipt.") || msg.type.startsWith("handshake.") || msg.type.startsWith("session.")) {
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
try {
|
|
2178
|
+
const senderPkStr = this._contactBook.getPublicKey(msg.fromAddress);
|
|
2179
|
+
if (senderPkStr === null) return;
|
|
2180
|
+
const senderVk = deserializeVerifyKey(senderPkStr);
|
|
2181
|
+
const receiptPayload = JSON.stringify({
|
|
2182
|
+
message_id: msg.messageId
|
|
2183
|
+
});
|
|
2184
|
+
const envelope = await createEnvelope(
|
|
2185
|
+
this._address,
|
|
2186
|
+
msg.fromAddress,
|
|
2187
|
+
"receipt.read" /* RECEIPT_READ */,
|
|
2188
|
+
new TextEncoder().encode(receiptPayload),
|
|
2189
|
+
this._keyManager.signingKey,
|
|
2190
|
+
senderVk
|
|
2191
|
+
);
|
|
2192
|
+
const wire = toWireDict(envelope);
|
|
2193
|
+
await this._transport.send(wire);
|
|
2194
|
+
} catch {
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Register with the relay server.
|
|
2199
|
+
*
|
|
2200
|
+
* Calls POST /api/v1/register with agent name and public key.
|
|
2201
|
+
* Stores the returned API key on disk.
|
|
2202
|
+
*/
|
|
2203
|
+
async _registerWithRelay() {
|
|
2204
|
+
const publicKeyStr = serializeVerifyKey(this._keyManager.verifyKey);
|
|
2205
|
+
const resp = await fetch(
|
|
2206
|
+
`${this._config.relayUrl}/api/v1/register`,
|
|
2207
|
+
{
|
|
2208
|
+
method: "POST",
|
|
2209
|
+
headers: { "Content-Type": "application/json" },
|
|
2210
|
+
body: JSON.stringify({
|
|
2211
|
+
agent_name: this._config.name,
|
|
2212
|
+
public_key: publicKeyStr
|
|
2213
|
+
})
|
|
2214
|
+
}
|
|
2215
|
+
);
|
|
2216
|
+
if (resp.status === 409) {
|
|
2217
|
+
throw new UAMError(
|
|
2218
|
+
`Address already registered with a different key: ${this._config.name}::${this._config.relayDomain}`
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
if (!resp.ok) {
|
|
2222
|
+
const text = await resp.text();
|
|
2223
|
+
throw new UAMError(
|
|
2224
|
+
`Registration failed: ${resp.status} ${text}`
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
const data = await resp.json();
|
|
2228
|
+
this._address = data["address"];
|
|
2229
|
+
this._token = data["token"];
|
|
2230
|
+
this._keyManager.saveToken(this._config.name, this._token);
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
|
|
2234
|
+
// src/sdk/webhook-verify.ts
|
|
2235
|
+
var import_node_crypto3 = require("crypto");
|
|
2236
|
+
|
|
2237
|
+
// src/sdk/index.ts
|
|
2238
|
+
init_dns_verifier();
|
|
2239
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2240
|
+
0 && (module.exports = {
|
|
2241
|
+
Agent,
|
|
2242
|
+
protocol
|
|
2243
|
+
});
|
|
2244
|
+
//# sourceMappingURL=index.cjs.map
|