@youam/sdk 0.2.0 → 0.4.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/LICENSE +190 -0
- package/README.md +7 -2
- package/dist/cli/index.cjs +1179 -195
- package/dist/index.cjs +1787 -135
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +399 -19
- package/dist/index.d.ts +399 -19
- package/dist/index.js +1779 -133
- package/dist/index.js.map +1 -1
- package/package.json +10 -7
package/dist/cli/index.cjs
CHANGED
|
@@ -60,11 +60,11 @@ function extractPublicKey(tags) {
|
|
|
60
60
|
}
|
|
61
61
|
return null;
|
|
62
62
|
}
|
|
63
|
-
async function queryUamTxt(
|
|
63
|
+
async function queryUamTxt(domain2, timeout) {
|
|
64
64
|
const controller = new AbortController();
|
|
65
65
|
const timer = timeout != null ? setTimeout(() => controller.abort(), timeout) : void 0;
|
|
66
66
|
try {
|
|
67
|
-
const records = await (0, import_promises.resolveTxt)(`_uam.${
|
|
67
|
+
const records = await (0, import_promises.resolveTxt)(`_uam.${domain2}`);
|
|
68
68
|
const results = [];
|
|
69
69
|
for (const parts of records) {
|
|
70
70
|
const txtValue = parts.join("");
|
|
@@ -79,8 +79,8 @@ async function queryUamTxt(domain, timeout) {
|
|
|
79
79
|
if (timer != null) clearTimeout(timer);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
async function resolveKeyViaHttps(agentName,
|
|
83
|
-
const url = `https://${
|
|
82
|
+
async function resolveKeyViaHttps(agentName, domain2, timeout) {
|
|
83
|
+
const url = `https://${domain2}/.well-known/uam.json`;
|
|
84
84
|
try {
|
|
85
85
|
const resp = await fetch(url, {
|
|
86
86
|
signal: AbortSignal.timeout(timeout ?? 1e4)
|
|
@@ -114,14 +114,18 @@ var init_dns_verifier = __esm({
|
|
|
114
114
|
// src/cli/index.ts
|
|
115
115
|
var import_commander = require("commander");
|
|
116
116
|
|
|
117
|
+
// src/version.ts
|
|
118
|
+
var SDK_VERSION = "0.4.0";
|
|
119
|
+
|
|
117
120
|
// src/cli/commands/init.ts
|
|
118
121
|
var import_node_os5 = require("os");
|
|
119
|
-
var
|
|
120
|
-
var
|
|
122
|
+
var import_node_fs6 = require("fs");
|
|
123
|
+
var import_node_path7 = require("path");
|
|
121
124
|
|
|
122
125
|
// src/protocol/types.ts
|
|
123
126
|
var UAM_VERSION = "0.1";
|
|
124
127
|
var MAX_ENVELOPE_SIZE = 65536;
|
|
128
|
+
var MAX_ENVELOPE_AGE = 300;
|
|
125
129
|
function b64Encode(data) {
|
|
126
130
|
return Buffer.from(data).toString("base64url");
|
|
127
131
|
}
|
|
@@ -195,6 +199,18 @@ var KeyPinningError = class _KeyPinningError extends UAMError {
|
|
|
195
199
|
Object.setPrototypeOf(this, _KeyPinningError.prototype);
|
|
196
200
|
}
|
|
197
201
|
};
|
|
202
|
+
var SendRefusedError = class extends UAMError {
|
|
203
|
+
messageId;
|
|
204
|
+
code;
|
|
205
|
+
detail;
|
|
206
|
+
constructor(messageId, code, detail) {
|
|
207
|
+
super(`Relay refused message ${messageId}: [${code}] ${detail ?? ""}`.trimEnd());
|
|
208
|
+
this.name = "SendRefusedError";
|
|
209
|
+
this.messageId = messageId;
|
|
210
|
+
this.code = code;
|
|
211
|
+
this.detail = detail;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
198
214
|
|
|
199
215
|
// src/protocol/address.ts
|
|
200
216
|
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])?)$/;
|
|
@@ -321,11 +337,11 @@ function encryptPayload(plaintext, senderSigningKey, recipientVerifyKey) {
|
|
|
321
337
|
try {
|
|
322
338
|
const senderCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_sk_to_curve25519(senderSigningKey);
|
|
323
339
|
const recipientCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_pk_to_curve25519(recipientVerifyKey);
|
|
324
|
-
const
|
|
325
|
-
const encrypted = import_libsodium_wrappers.default.crypto_box_easy(plaintext,
|
|
326
|
-
const combined = new Uint8Array(
|
|
327
|
-
combined.set(
|
|
328
|
-
combined.set(encrypted,
|
|
340
|
+
const nonce2 = import_libsodium_wrappers.default.randombytes_buf(import_libsodium_wrappers.default.crypto_box_NONCEBYTES);
|
|
341
|
+
const encrypted = import_libsodium_wrappers.default.crypto_box_easy(plaintext, nonce2, recipientCurve, senderCurve);
|
|
342
|
+
const combined = new Uint8Array(nonce2.length + encrypted.length);
|
|
343
|
+
combined.set(nonce2, 0);
|
|
344
|
+
combined.set(encrypted, nonce2.length);
|
|
329
345
|
return b64Encode(combined);
|
|
330
346
|
} catch (err) {
|
|
331
347
|
if (err instanceof EncryptionError) throw err;
|
|
@@ -335,11 +351,11 @@ function encryptPayload(plaintext, senderSigningKey, recipientVerifyKey) {
|
|
|
335
351
|
function decryptPayload(ciphertextB64, recipientSigningKey, senderVerifyKey) {
|
|
336
352
|
try {
|
|
337
353
|
const combined = b64Decode(ciphertextB64);
|
|
338
|
-
const
|
|
354
|
+
const nonce2 = combined.slice(0, import_libsodium_wrappers.default.crypto_box_NONCEBYTES);
|
|
339
355
|
const ciphertext = combined.slice(import_libsodium_wrappers.default.crypto_box_NONCEBYTES);
|
|
340
356
|
const recipientCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_sk_to_curve25519(recipientSigningKey);
|
|
341
357
|
const senderCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_pk_to_curve25519(senderVerifyKey);
|
|
342
|
-
return import_libsodium_wrappers.default.crypto_box_open_easy(ciphertext,
|
|
358
|
+
return import_libsodium_wrappers.default.crypto_box_open_easy(ciphertext, nonce2, senderCurve, recipientCurve);
|
|
343
359
|
} catch (err) {
|
|
344
360
|
if (err instanceof DecryptionError) throw err;
|
|
345
361
|
throw new DecryptionError(err instanceof Error ? err.message : String(err));
|
|
@@ -447,7 +463,7 @@ async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaint
|
|
|
447
463
|
parseAddress(fromAddress);
|
|
448
464
|
parseAddress(toAddress);
|
|
449
465
|
const messageId = (0, import_node_crypto2.randomUUID)();
|
|
450
|
-
const
|
|
466
|
+
const nonce2 = generateNonce();
|
|
451
467
|
const timestamp = utcTimestamp();
|
|
452
468
|
let encryptedPayload;
|
|
453
469
|
if (typeValue === "handshake.request" /* HANDSHAKE_REQUEST */) {
|
|
@@ -466,7 +482,7 @@ async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaint
|
|
|
466
482
|
toAddress,
|
|
467
483
|
timestamp,
|
|
468
484
|
type: typeValue,
|
|
469
|
-
nonce,
|
|
485
|
+
nonce: nonce2,
|
|
470
486
|
payload: encryptedPayload,
|
|
471
487
|
signature: "",
|
|
472
488
|
threadId: options?.threadId,
|
|
@@ -485,7 +501,7 @@ async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaint
|
|
|
485
501
|
toAddress,
|
|
486
502
|
timestamp,
|
|
487
503
|
type: typeValue,
|
|
488
|
-
nonce,
|
|
504
|
+
nonce: nonce2,
|
|
489
505
|
payload: encryptedPayload,
|
|
490
506
|
signature,
|
|
491
507
|
threadId: options?.threadId,
|
|
@@ -577,8 +593,8 @@ function contactCardFromDict(d, options) {
|
|
|
577
593
|
}
|
|
578
594
|
return card;
|
|
579
595
|
}
|
|
580
|
-
async function createContactCard(
|
|
581
|
-
parseAddress(
|
|
596
|
+
async function createContactCard(address2, displayName, relay, signingKey, options) {
|
|
597
|
+
parseAddress(address2);
|
|
582
598
|
const verifyKey = signingKey.slice(32);
|
|
583
599
|
const pubKeyB64 = serializeVerifyKey(verifyKey);
|
|
584
600
|
const fp = publicKeyFingerprint(verifyKey);
|
|
@@ -588,7 +604,7 @@ async function createContactCard(address, displayName, relay, signingKey, option
|
|
|
588
604
|
];
|
|
589
605
|
const tempCard = {
|
|
590
606
|
version: UAM_VERSION,
|
|
591
|
-
address,
|
|
607
|
+
address: address2,
|
|
592
608
|
displayName,
|
|
593
609
|
description: options?.description,
|
|
594
610
|
system: options?.system,
|
|
@@ -605,7 +621,7 @@ async function createContactCard(address, displayName, relay, signingKey, option
|
|
|
605
621
|
const signature = signMessage(canonicalize(signable), signingKey);
|
|
606
622
|
return Object.freeze({
|
|
607
623
|
version: UAM_VERSION,
|
|
608
|
-
address,
|
|
624
|
+
address: address2,
|
|
609
625
|
displayName,
|
|
610
626
|
description: options?.description,
|
|
611
627
|
system: options?.system,
|
|
@@ -632,10 +648,163 @@ function verifyContactCard(card) {
|
|
|
632
648
|
verifySignature(canonicalize(signable), card.signature, vk);
|
|
633
649
|
}
|
|
634
650
|
|
|
651
|
+
// src/protocol/signed.ts
|
|
652
|
+
var import_node_crypto3 = require("crypto");
|
|
653
|
+
|
|
654
|
+
// src/protocol/didkey.ts
|
|
655
|
+
var B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
656
|
+
var PREFIX = {
|
|
657
|
+
ed25519: new Uint8Array([237, 1]),
|
|
658
|
+
p256: new Uint8Array([128, 36])
|
|
659
|
+
};
|
|
660
|
+
var LENGTH = { ed25519: 32, p256: 33 };
|
|
661
|
+
var InvalidDidKeyError = class extends UAMError {
|
|
662
|
+
constructor(message) {
|
|
663
|
+
super(message);
|
|
664
|
+
this.name = "InvalidDidKeyError";
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
function b58decode(text) {
|
|
668
|
+
let n = 0n;
|
|
669
|
+
for (const c of text) {
|
|
670
|
+
const i = B58.indexOf(c);
|
|
671
|
+
if (i < 0) throw new InvalidDidKeyError(`not base58btc: ${JSON.stringify(c)}`);
|
|
672
|
+
n = n * 58n + BigInt(i);
|
|
673
|
+
}
|
|
674
|
+
const body = [];
|
|
675
|
+
while (n > 0n) {
|
|
676
|
+
body.unshift(Number(n & 0xffn));
|
|
677
|
+
n >>= 8n;
|
|
678
|
+
}
|
|
679
|
+
let zeros = 0;
|
|
680
|
+
while (zeros < text.length && text[zeros] === "1") zeros += 1;
|
|
681
|
+
return new Uint8Array([...new Array(zeros).fill(0), ...body]);
|
|
682
|
+
}
|
|
683
|
+
function checkKey(kind, key) {
|
|
684
|
+
if (key.length !== LENGTH[kind]) {
|
|
685
|
+
throw new InvalidDidKeyError(`${kind} key must be ${LENGTH[kind]} bytes`);
|
|
686
|
+
}
|
|
687
|
+
if (kind === "p256" && key[0] !== 2 && key[0] !== 3) {
|
|
688
|
+
throw new InvalidDidKeyError("P-256 key must be a compressed point");
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function decodeDidKey(did2) {
|
|
692
|
+
if (typeof did2 !== "string" || !did2.startsWith("did:key:z")) {
|
|
693
|
+
throw new InvalidDidKeyError("not a did:key with base58btc multibase");
|
|
694
|
+
}
|
|
695
|
+
const raw = b58decode(did2.slice("did:key:z".length));
|
|
696
|
+
for (const kind of Object.keys(PREFIX)) {
|
|
697
|
+
const prefix = PREFIX[kind];
|
|
698
|
+
if (raw.length >= prefix.length && prefix.every((b, i) => raw[i] === b)) {
|
|
699
|
+
const key = raw.slice(prefix.length);
|
|
700
|
+
checkKey(kind, key);
|
|
701
|
+
return { kind, key };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
throw new InvalidDidKeyError("unsupported key type");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/protocol/signed.ts
|
|
708
|
+
var MAX_INT = 2 ** 53 - 1;
|
|
709
|
+
var SignedObjectError = class extends UAMError {
|
|
710
|
+
constructor(message) {
|
|
711
|
+
super(message);
|
|
712
|
+
this.name = "SignedObjectError";
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
var has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
716
|
+
function isPlainObject(value) {
|
|
717
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
718
|
+
const proto = Object.getPrototypeOf(value);
|
|
719
|
+
return proto === null || proto === Object.prototype;
|
|
720
|
+
}
|
|
721
|
+
var HASH_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
722
|
+
var OWNER_ID_RE = /^[A-Za-z0-9_-]{22}$/;
|
|
723
|
+
var NONCE_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
724
|
+
var SECRET_RE = /^[A-Za-z0-9_-]{22}$/;
|
|
725
|
+
var DOMAIN_RE = /^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
726
|
+
function fail(path, why) {
|
|
727
|
+
throw new SignedObjectError(`${path}: ${why}`);
|
|
728
|
+
}
|
|
729
|
+
var matching = (re, what) => (v, p) => {
|
|
730
|
+
if (typeof v !== "string" || !re.test(v)) fail(p, `must be ${what}`);
|
|
731
|
+
};
|
|
732
|
+
var hash = matching(HASH_RE, "a hash (43 base64url characters)");
|
|
733
|
+
var ownerId = matching(OWNER_ID_RE, "an owner id (22 base64url characters)");
|
|
734
|
+
var nonce = matching(NONCE_RE, "a nonce (16-64 base64url characters)");
|
|
735
|
+
var secret = matching(SECRET_RE, "a secret (22 base64url characters)");
|
|
736
|
+
var domain = matching(DOMAIN_RE, "a lowercase domain");
|
|
737
|
+
var constant = (expected) => (v, p) => {
|
|
738
|
+
if (v !== expected) fail(p, `must be ${JSON.stringify(expected)}`);
|
|
739
|
+
};
|
|
740
|
+
var oneOf = (...options) => (v, p) => {
|
|
741
|
+
if (typeof v !== "string" || !options.includes(v)) fail(p, `must be one of ${options.join(", ")}`);
|
|
742
|
+
};
|
|
743
|
+
var address = (v, p) => {
|
|
744
|
+
if (typeof v !== "string") fail(p, "must be an address");
|
|
745
|
+
let full;
|
|
746
|
+
try {
|
|
747
|
+
full = parseAddress(v).full;
|
|
748
|
+
} catch {
|
|
749
|
+
return fail(p, "must be an address");
|
|
750
|
+
}
|
|
751
|
+
if (full !== v) fail(p, "must be an address in canonical (lowercase) form");
|
|
752
|
+
};
|
|
753
|
+
var did = (kind) => (v, p) => {
|
|
754
|
+
let found;
|
|
755
|
+
try {
|
|
756
|
+
found = decodeDidKey(v).kind;
|
|
757
|
+
} catch (err) {
|
|
758
|
+
return fail(p, err instanceof Error ? err.message : String(err));
|
|
759
|
+
}
|
|
760
|
+
if (found !== kind) fail(p, `must be a ${kind} did:key`);
|
|
761
|
+
};
|
|
762
|
+
var list = (item, minLen = 1, maxLen = 64) => (v, p) => {
|
|
763
|
+
if (!Array.isArray(v) || v.length < minLen || v.length > maxLen) {
|
|
764
|
+
fail(p, `must be a list of ${minLen}-${maxLen} items`);
|
|
765
|
+
}
|
|
766
|
+
v.forEach((entry, i) => item(entry, `${p}[${i}]`));
|
|
767
|
+
};
|
|
768
|
+
var object = (required, optional = {}) => (v, p) => {
|
|
769
|
+
if (!isPlainObject(v)) fail(p, "must be an object");
|
|
770
|
+
for (const key of Object.keys(v)) {
|
|
771
|
+
if (!has(required, key) && !has(optional, key)) fail(`${p}.${key}`, "unknown member");
|
|
772
|
+
}
|
|
773
|
+
for (const [key, member] of Object.entries(required)) {
|
|
774
|
+
if (!has(v, key)) fail(`${p}.${key}`, "missing");
|
|
775
|
+
member(v[key], `${p}.${key}`);
|
|
776
|
+
}
|
|
777
|
+
for (const [key, member] of Object.entries(optional)) {
|
|
778
|
+
if (has(v, key)) member(v[key], `${p}.${key}`);
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
var location = (v, p) => {
|
|
782
|
+
if (v !== "shared") address(v, p);
|
|
783
|
+
};
|
|
784
|
+
var access = object({ actions: list(oneOf("message"), 1, 1), locations: list(location) });
|
|
785
|
+
var ownerRef = object({ relay: domain, ownerId, keysHash: hash });
|
|
786
|
+
var ownerRefNoKeys = object({ relay: domain, ownerId });
|
|
787
|
+
var passkey = did("p256");
|
|
788
|
+
var agentKey = did("ed25519");
|
|
789
|
+
var keysOwner = object({ relay: domain }, { ownerId });
|
|
790
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
791
|
+
var SUBJECTS = {
|
|
792
|
+
owner: object({ kind: constant("owner"), owner: ownerRef }),
|
|
793
|
+
invitee: object({ kind: constant("invitee"), invite: hash }),
|
|
794
|
+
agent: object({ kind: constant("agent"), id: agentKey, address })
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
// src/protocol/webauthn.ts
|
|
798
|
+
var import_node_crypto4 = require("crypto");
|
|
799
|
+
var import_p256 = require("@noble/curves/p256");
|
|
800
|
+
|
|
635
801
|
// src/sdk/config.ts
|
|
636
802
|
var import_node_os = require("os");
|
|
637
803
|
var import_node_path = require("path");
|
|
638
804
|
var DEFAULT_RELAY_URL = "https://relay.youam.network";
|
|
805
|
+
function trimRelayUrl(url) {
|
|
806
|
+
return url.replace(/\/+$/, "");
|
|
807
|
+
}
|
|
639
808
|
var VALID_POLICIES = /* @__PURE__ */ new Set([
|
|
640
809
|
"auto-accept",
|
|
641
810
|
"approval-required",
|
|
@@ -652,9 +821,13 @@ var SDKConfig = class {
|
|
|
652
821
|
transportType;
|
|
653
822
|
trustPolicy;
|
|
654
823
|
relayDomain;
|
|
824
|
+
/** The domain the caller chose, if any; null when only derived. */
|
|
825
|
+
explicitRelayDomain;
|
|
655
826
|
constructor(options) {
|
|
656
827
|
this.name = options.name;
|
|
657
|
-
this.relayUrl =
|
|
828
|
+
this.relayUrl = trimRelayUrl(
|
|
829
|
+
options.relayUrl ?? process.env["UAM_RELAY_URL"] ?? DEFAULT_RELAY_URL
|
|
830
|
+
);
|
|
658
831
|
if (options.relayWsUrl) {
|
|
659
832
|
this.relayWsUrl = options.relayWsUrl;
|
|
660
833
|
} else {
|
|
@@ -665,6 +838,7 @@ var SDKConfig = class {
|
|
|
665
838
|
this.relayWsUrl = wsUrl;
|
|
666
839
|
}
|
|
667
840
|
const envDomain = process.env["UAM_RELAY_DOMAIN"];
|
|
841
|
+
this.explicitRelayDomain = envDomain || options.relayDomain || null;
|
|
668
842
|
if (envDomain) {
|
|
669
843
|
this.relayDomain = envDomain;
|
|
670
844
|
} else if (options.relayDomain) {
|
|
@@ -768,17 +942,54 @@ var KeyManager = class {
|
|
|
768
942
|
(0, import_node_fs.writeFileSync)(tokenPath, token);
|
|
769
943
|
this._setPermissions(tokenPath);
|
|
770
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* Store the address `relayUrl` gave this agent.
|
|
947
|
+
*/
|
|
948
|
+
saveAddress(name, address2, relayUrl) {
|
|
949
|
+
(0, import_node_fs.mkdirSync)(this._keyDir, { recursive: true });
|
|
950
|
+
(0, import_node_fs.writeFileSync)(
|
|
951
|
+
(0, import_node_path2.join)(this._keyDir, `${name}.address`),
|
|
952
|
+
JSON.stringify({ address: address2, relay_url: relayUrl })
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Load the address saved for `relayUrl`.
|
|
957
|
+
*
|
|
958
|
+
* Returns null if there is none: the file is missing, unreadable or
|
|
959
|
+
* malformed, or it belongs to another relay.
|
|
960
|
+
*/
|
|
961
|
+
loadAddress(name, relayUrl) {
|
|
962
|
+
try {
|
|
963
|
+
const path = (0, import_node_path2.join)(this._keyDir, `${name}.address`);
|
|
964
|
+
const saved = JSON.parse((0, import_node_fs.readFileSync)(path, "utf-8"));
|
|
965
|
+
const address2 = saved?.address;
|
|
966
|
+
const savedRelay = saved?.relay_url;
|
|
967
|
+
if (typeof address2 !== "string" || typeof savedRelay !== "string") {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
if (trimRelayUrl(savedRelay) !== trimRelayUrl(relayUrl)) return null;
|
|
971
|
+
parseAddress(address2);
|
|
972
|
+
return address2;
|
|
973
|
+
} catch {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
771
977
|
/**
|
|
772
978
|
* Load a previously saved token, or return null.
|
|
773
979
|
*
|
|
774
980
|
* Checks UAM_TOKEN env var first, then file-based storage,
|
|
775
|
-
* then legacy .api_key files for backward compatibility.
|
|
981
|
+
* then legacy .api_key files for backward compatibility. With `relayUrl`,
|
|
982
|
+
* a saved token that belongs to another relay counts as none, so it is
|
|
983
|
+
* only offered to the relay that issued it.
|
|
776
984
|
*/
|
|
777
|
-
loadToken(name) {
|
|
985
|
+
loadToken(name, relayUrl) {
|
|
778
986
|
const envToken = process.env["UAM_TOKEN"];
|
|
779
987
|
if (envToken) {
|
|
780
988
|
return envToken.trim();
|
|
781
989
|
}
|
|
990
|
+
if (relayUrl !== void 0 && this._savedForAnotherRelay(name, relayUrl)) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
782
993
|
const tokenPath = (0, import_node_path2.join)(this._keyDir, `${name}.token`);
|
|
783
994
|
if ((0, import_node_fs2.existsSync)(tokenPath)) {
|
|
784
995
|
return (0, import_node_fs.readFileSync)(tokenPath, "utf-8").trim();
|
|
@@ -789,6 +1000,20 @@ var KeyManager = class {
|
|
|
789
1000
|
}
|
|
790
1001
|
return null;
|
|
791
1002
|
}
|
|
1003
|
+
/**
|
|
1004
|
+
* True if the saved address says this agent's files are another relay's.
|
|
1005
|
+
* A missing or damaged file says nothing.
|
|
1006
|
+
*/
|
|
1007
|
+
_savedForAnotherRelay(name, relayUrl) {
|
|
1008
|
+
try {
|
|
1009
|
+
const path = (0, import_node_path2.join)(this._keyDir, `${name}.address`);
|
|
1010
|
+
const saved = JSON.parse((0, import_node_fs.readFileSync)(path, "utf-8"));
|
|
1011
|
+
const savedRelay = saved?.relay_url;
|
|
1012
|
+
return typeof savedRelay === "string" && trimRelayUrl(savedRelay) !== trimRelayUrl(relayUrl);
|
|
1013
|
+
} catch {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
792
1017
|
/** Set file permissions to 0o600 (owner read/write only). */
|
|
793
1018
|
_setPermissions(path) {
|
|
794
1019
|
if ((0, import_node_os3.platform)() !== "win32") {
|
|
@@ -812,10 +1037,73 @@ var KeyManager = class {
|
|
|
812
1037
|
}
|
|
813
1038
|
};
|
|
814
1039
|
|
|
1040
|
+
// src/sdk/agent.ts
|
|
1041
|
+
var import_node_path5 = require("path");
|
|
1042
|
+
|
|
1043
|
+
// src/sdk/freshness.ts
|
|
1044
|
+
var RELAY_META_FEATURE = "relay_meta";
|
|
1045
|
+
var STORED_MAIL_MAX_AGE = 7 * 86400;
|
|
1046
|
+
var ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
1047
|
+
function relayVouches(raw, signedAt, now, relayDomain) {
|
|
1048
|
+
const meta = raw["relay_meta"];
|
|
1049
|
+
if (typeof meta !== "object" || meta === null || Array.isArray(meta)) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
const stamp = meta;
|
|
1053
|
+
if (stamp["v"] !== 1 || stamp["relay"] !== relayDomain) return false;
|
|
1054
|
+
if (stamp["stored"] !== true) return false;
|
|
1055
|
+
const received = stamp["received_at"];
|
|
1056
|
+
if (typeof received !== "string" || !ISO_TIME.test(received)) return false;
|
|
1057
|
+
const receivedAt = Date.parse(received);
|
|
1058
|
+
if (Number.isNaN(receivedAt)) return false;
|
|
1059
|
+
const window = MAX_ENVELOPE_AGE * 1e3;
|
|
1060
|
+
const age = now - signedAt;
|
|
1061
|
+
return Math.abs(receivedAt - signedAt) <= window && age >= 0 && age <= STORED_MAIL_MAX_AGE * 1e3 + window;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/sdk/message.ts
|
|
1065
|
+
var SEND_STATUSES = [
|
|
1066
|
+
"delivered",
|
|
1067
|
+
"stored",
|
|
1068
|
+
"relayed",
|
|
1069
|
+
"queued",
|
|
1070
|
+
"duplicate",
|
|
1071
|
+
"dropped",
|
|
1072
|
+
"pending",
|
|
1073
|
+
"accepted",
|
|
1074
|
+
"unknown"
|
|
1075
|
+
];
|
|
1076
|
+
function sendStatus(answer) {
|
|
1077
|
+
if (answer === null) return "unknown";
|
|
1078
|
+
const status = answer["status"];
|
|
1079
|
+
if (typeof status === "string" && SEND_STATUSES.includes(status) && status !== "accepted" && status !== "unknown") {
|
|
1080
|
+
return status;
|
|
1081
|
+
}
|
|
1082
|
+
return "accepted";
|
|
1083
|
+
}
|
|
1084
|
+
var PENDING_NOTE = "Not delivered: {to} only accepts messages from approved contacts. Do not resend it.";
|
|
1085
|
+
function pendingNote(to) {
|
|
1086
|
+
return PENDING_NOTE.replace("{to}", to);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
815
1089
|
// src/sdk/contact-book.ts
|
|
816
1090
|
var import_node_fs3 = require("fs");
|
|
817
1091
|
var import_node_path3 = require("path");
|
|
818
1092
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
1093
|
+
|
|
1094
|
+
// src/sdk/log.ts
|
|
1095
|
+
var log = {
|
|
1096
|
+
warn(message) {
|
|
1097
|
+
console.warn(`[uam] ${message}`);
|
|
1098
|
+
},
|
|
1099
|
+
info(message) {
|
|
1100
|
+
if (process.env["UAM_DEBUG"]) {
|
|
1101
|
+
console.info(`[uam] ${message}`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
// src/sdk/contact-book.ts
|
|
819
1107
|
var ContactBook = class {
|
|
820
1108
|
_dbPath;
|
|
821
1109
|
_db = null;
|
|
@@ -895,36 +1183,36 @@ var ContactBook = class {
|
|
|
895
1183
|
/**
|
|
896
1184
|
* Check if an address is in the contact book (in-memory, no I/O).
|
|
897
1185
|
*/
|
|
898
|
-
isKnown(
|
|
899
|
-
return this._knownAddresses.has(
|
|
1186
|
+
isKnown(address2) {
|
|
1187
|
+
return this._knownAddresses.has(address2);
|
|
900
1188
|
}
|
|
901
1189
|
/**
|
|
902
1190
|
* Check if an address matches any blocked pattern (O(1) set lookup).
|
|
903
1191
|
*/
|
|
904
|
-
isBlocked(
|
|
905
|
-
if (this._blockedExact.has(
|
|
1192
|
+
isBlocked(address2) {
|
|
1193
|
+
if (this._blockedExact.has(address2)) {
|
|
906
1194
|
return true;
|
|
907
1195
|
}
|
|
908
|
-
if (
|
|
909
|
-
const
|
|
910
|
-
return this._blockedDomains.has(
|
|
1196
|
+
if (address2.includes("::")) {
|
|
1197
|
+
const domain2 = address2.split("::")[1];
|
|
1198
|
+
return this._blockedDomains.has(domain2);
|
|
911
1199
|
}
|
|
912
1200
|
return false;
|
|
913
1201
|
}
|
|
914
1202
|
/**
|
|
915
1203
|
* Look up the public key for a known contact.
|
|
916
1204
|
*/
|
|
917
|
-
getPublicKey(
|
|
1205
|
+
getPublicKey(address2) {
|
|
918
1206
|
if (this._db === null) return null;
|
|
919
|
-
const row = this._db.prepare("SELECT public_key FROM contacts WHERE address = ?").get(
|
|
1207
|
+
const row = this._db.prepare("SELECT public_key FROM contacts WHERE address = ?").get(address2);
|
|
920
1208
|
return row?.public_key ?? null;
|
|
921
1209
|
}
|
|
922
1210
|
/**
|
|
923
1211
|
* Return the relay URLs for a known contact (CARD-04).
|
|
924
1212
|
*/
|
|
925
|
-
getRelayUrls(
|
|
1213
|
+
getRelayUrls(address2) {
|
|
926
1214
|
if (this._db === null) return null;
|
|
927
|
-
const row = this._db.prepare("SELECT relay, relays_json FROM contacts WHERE address = ?").get(
|
|
1215
|
+
const row = this._db.prepare("SELECT relay, relays_json FROM contacts WHERE address = ?").get(address2);
|
|
928
1216
|
if (!row) return null;
|
|
929
1217
|
if (row.relays_json !== null) {
|
|
930
1218
|
return JSON.parse(row.relays_json);
|
|
@@ -936,11 +1224,30 @@ var ContactBook = class {
|
|
|
936
1224
|
}
|
|
937
1225
|
/**
|
|
938
1226
|
* Add or update a contact (upsert).
|
|
1227
|
+
*
|
|
1228
|
+
* Refuses to replace the key of a contact whose trust_state is "pinned"
|
|
1229
|
+
* or "verified" with a different key, unless `force: true` is passed (for
|
|
1230
|
+
* a deliberate key rotation, which is logged). Matches the Python SDK's
|
|
1231
|
+
* ContactBook.add_contact.
|
|
1232
|
+
*
|
|
1233
|
+
* @throws {KeyPinningError} If a pinned/verified contact's key would be
|
|
1234
|
+
* replaced by a different key without `force`.
|
|
939
1235
|
*/
|
|
940
|
-
addContact(
|
|
1236
|
+
addContact(address2, publicKey, options) {
|
|
941
1237
|
if (this._db === null) {
|
|
942
1238
|
throw new Error("ContactBook not open. Call open() first.");
|
|
943
1239
|
}
|
|
1240
|
+
const existing = this._db.prepare("SELECT public_key, trust_state FROM contacts WHERE address = ?").get(address2);
|
|
1241
|
+
if (existing !== void 0 && (existing.trust_state === "pinned" || existing.trust_state === "verified") && existing.public_key !== publicKey) {
|
|
1242
|
+
if (!options?.force) {
|
|
1243
|
+
throw new KeyPinningError(
|
|
1244
|
+
`Refusing to overwrite ${existing.trust_state} contact ${JSON.stringify(address2)}: stored public key does not match the new key. Pass force: true to rotate.`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
log.warn(
|
|
1248
|
+
`Force-replacing the key of ${existing.trust_state} contact ${address2} (key rotation)`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
944
1251
|
const displayName = options?.displayName ?? null;
|
|
945
1252
|
const trustState = options?.trustState ?? "trusted";
|
|
946
1253
|
const trustSource = options?.trustSource ?? null;
|
|
@@ -957,8 +1264,8 @@ var ContactBook = class {
|
|
|
957
1264
|
relay = COALESCE(excluded.relay, contacts.relay),
|
|
958
1265
|
relays_json = COALESCE(excluded.relays_json, contacts.relays_json),
|
|
959
1266
|
last_seen = datetime('now')`
|
|
960
|
-
).run(
|
|
961
|
-
this._knownAddresses.add(
|
|
1267
|
+
).run(address2, publicKey, displayName, trustState, trustSource, relay, relaysJson);
|
|
1268
|
+
this._knownAddresses.add(address2);
|
|
962
1269
|
}
|
|
963
1270
|
/**
|
|
964
1271
|
* Return all contacts sorted by last_seen DESC.
|
|
@@ -979,13 +1286,13 @@ var ContactBook = class {
|
|
|
979
1286
|
/**
|
|
980
1287
|
* Store a pending handshake request.
|
|
981
1288
|
*/
|
|
982
|
-
addPending(
|
|
1289
|
+
addPending(address2, contactCardJson) {
|
|
983
1290
|
if (this._db === null) {
|
|
984
1291
|
throw new Error("ContactBook not open. Call open() first.");
|
|
985
1292
|
}
|
|
986
1293
|
this._db.prepare(
|
|
987
1294
|
`INSERT OR REPLACE INTO pending_handshakes (address, contact_card) VALUES (?, ?)`
|
|
988
|
-
).run(
|
|
1295
|
+
).run(address2, contactCardJson);
|
|
989
1296
|
}
|
|
990
1297
|
/**
|
|
991
1298
|
* Retrieve all pending handshake requests.
|
|
@@ -1004,9 +1311,9 @@ var ContactBook = class {
|
|
|
1004
1311
|
/**
|
|
1005
1312
|
* Remove a pending handshake request.
|
|
1006
1313
|
*/
|
|
1007
|
-
removePending(
|
|
1314
|
+
removePending(address2) {
|
|
1008
1315
|
if (this._db === null) return;
|
|
1009
|
-
this._db.prepare("DELETE FROM pending_handshakes WHERE address = ?").run(
|
|
1316
|
+
this._db.prepare("DELETE FROM pending_handshakes WHERE address = ?").run(address2);
|
|
1010
1317
|
}
|
|
1011
1318
|
/**
|
|
1012
1319
|
* Return pending handshakes older than N days.
|
|
@@ -1026,31 +1333,31 @@ var ContactBook = class {
|
|
|
1026
1333
|
/**
|
|
1027
1334
|
* Return the trust_state for an address, or null if unknown.
|
|
1028
1335
|
*/
|
|
1029
|
-
getTrustState(
|
|
1336
|
+
getTrustState(address2) {
|
|
1030
1337
|
if (this._db === null) return null;
|
|
1031
|
-
const row = this._db.prepare("SELECT trust_state FROM contacts WHERE address = ?").get(
|
|
1338
|
+
const row = this._db.prepare("SELECT trust_state FROM contacts WHERE address = ?").get(address2);
|
|
1032
1339
|
return row?.trust_state ?? null;
|
|
1033
1340
|
}
|
|
1034
1341
|
/**
|
|
1035
1342
|
* Set the pinned_at timestamp and trust_state='pinned' for a contact (TOFU).
|
|
1036
1343
|
*/
|
|
1037
|
-
setPinnedAt(
|
|
1344
|
+
setPinnedAt(address2) {
|
|
1038
1345
|
if (this._db === null) {
|
|
1039
1346
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1040
1347
|
}
|
|
1041
1348
|
this._db.prepare(
|
|
1042
1349
|
"UPDATE contacts SET pinned_at = datetime('now'), trust_state = 'pinned' WHERE address = ?"
|
|
1043
|
-
).run(
|
|
1350
|
+
).run(address2);
|
|
1044
1351
|
}
|
|
1045
1352
|
/**
|
|
1046
1353
|
* Remove a contact by address. Returns true if a contact was deleted.
|
|
1047
1354
|
*/
|
|
1048
|
-
removeContact(
|
|
1355
|
+
removeContact(address2) {
|
|
1049
1356
|
if (this._db === null) {
|
|
1050
1357
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1051
1358
|
}
|
|
1052
|
-
const result = this._db.prepare("DELETE FROM contacts WHERE address = ?").run(
|
|
1053
|
-
this._knownAddresses.delete(
|
|
1359
|
+
const result = this._db.prepare("DELETE FROM contacts WHERE address = ?").run(address2);
|
|
1360
|
+
this._knownAddresses.delete(address2);
|
|
1054
1361
|
return result.changes > 0;
|
|
1055
1362
|
}
|
|
1056
1363
|
/**
|
|
@@ -1058,8 +1365,8 @@ var ContactBook = class {
|
|
|
1058
1365
|
*
|
|
1059
1366
|
* Convenience helper for inbound message filtering (CARD-05, TOFU-02).
|
|
1060
1367
|
*/
|
|
1061
|
-
isTrustedOrVerified(
|
|
1062
|
-
const state = this.getTrustState(
|
|
1368
|
+
isTrustedOrVerified(address2) {
|
|
1369
|
+
const state = this.getTrustState(address2);
|
|
1063
1370
|
return state === "trusted" || state === "verified" || state === "pinned";
|
|
1064
1371
|
}
|
|
1065
1372
|
/**
|
|
@@ -1113,6 +1420,18 @@ var ContactBook = class {
|
|
|
1113
1420
|
};
|
|
1114
1421
|
|
|
1115
1422
|
// src/sdk/handshake.ts
|
|
1423
|
+
function bindCardToEnvelope(card, envelope, senderVk) {
|
|
1424
|
+
if (card.address !== envelope.fromAddress) {
|
|
1425
|
+
throw new InvalidEnvelopeError(
|
|
1426
|
+
`Contact card address ${JSON.stringify(card.address)} does not match envelope sender ${JSON.stringify(envelope.fromAddress)}`
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
if (card.publicKey !== serializeVerifyKey(senderVk)) {
|
|
1430
|
+
throw new InvalidEnvelopeError(
|
|
1431
|
+
"Contact card public_key does not match envelope sender's verify key"
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1116
1435
|
var HandshakeManager = class {
|
|
1117
1436
|
_contactBook;
|
|
1118
1437
|
_trustPolicy;
|
|
@@ -1155,7 +1474,7 @@ var HandshakeManager = class {
|
|
|
1155
1474
|
if (envelope.type === "handshake.request" /* HANDSHAKE_REQUEST */) {
|
|
1156
1475
|
await this._handleRequest(agent, envelope, senderVk);
|
|
1157
1476
|
} else if (envelope.type === "handshake.accept" /* HANDSHAKE_ACCEPT */) {
|
|
1158
|
-
await this._handleAccept(envelope, senderVk);
|
|
1477
|
+
await this._handleAccept(agent, envelope, senderVk);
|
|
1159
1478
|
} else if (envelope.type === "handshake.deny" /* HANDSHAKE_DENY */) {
|
|
1160
1479
|
}
|
|
1161
1480
|
return null;
|
|
@@ -1173,6 +1492,7 @@ var HandshakeManager = class {
|
|
|
1173
1492
|
);
|
|
1174
1493
|
const card = contactCardFromDict(cardDict);
|
|
1175
1494
|
verifyContactCard(card);
|
|
1495
|
+
bindCardToEnvelope(card, envelope, senderVk);
|
|
1176
1496
|
if (this._trustPolicy === "auto-accept") {
|
|
1177
1497
|
this._contactBook.addContact(card.address, card.publicKey, {
|
|
1178
1498
|
displayName: card.displayName,
|
|
@@ -1191,8 +1511,46 @@ var HandshakeManager = class {
|
|
|
1191
1511
|
}
|
|
1192
1512
|
/**
|
|
1193
1513
|
* Process a handshake.accept: store the sender as pinned (TOFU).
|
|
1514
|
+
*
|
|
1515
|
+
* Only honoured when:
|
|
1516
|
+
* 1. we previously sent this address a handshake.request
|
|
1517
|
+
* (trust_state === "handshake-sent"); anything else is an unsolicited
|
|
1518
|
+
* accept and is dropped. This also means an accept can never
|
|
1519
|
+
* downgrade a trusted/verified contact to pinned;
|
|
1520
|
+
* 2. the payload decrypts with our key and the sender's key, and parses
|
|
1521
|
+
* as JSON. Otherwise it is dropped without pinning;
|
|
1522
|
+
* 3. an embedded contact_card, if present, is well-formed, correctly
|
|
1523
|
+
* self-signed, and bound to the envelope sender (throws otherwise).
|
|
1194
1524
|
*/
|
|
1195
|
-
async _handleAccept(envelope, senderVk) {
|
|
1525
|
+
async _handleAccept(agent, envelope, senderVk) {
|
|
1526
|
+
const prior = this._contactBook.getTrustState(envelope.fromAddress);
|
|
1527
|
+
if (prior !== "handshake-sent") {
|
|
1528
|
+
log.warn(
|
|
1529
|
+
`Ignoring unsolicited handshake.accept from ${envelope.fromAddress} (trust_state=${JSON.stringify(prior)})`
|
|
1530
|
+
);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
let payload;
|
|
1534
|
+
try {
|
|
1535
|
+
const plaintext = decryptPayload(
|
|
1536
|
+
envelope.payload,
|
|
1537
|
+
agent._keyManager.signingKey,
|
|
1538
|
+
senderVk
|
|
1539
|
+
);
|
|
1540
|
+
payload = JSON.parse(new TextDecoder().decode(plaintext));
|
|
1541
|
+
} catch {
|
|
1542
|
+
log.warn(
|
|
1543
|
+
`Dropping handshake.accept ${envelope.messageId} from ${envelope.fromAddress}: payload could not be decrypted or parsed`
|
|
1544
|
+
);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (payload !== null && typeof payload === "object" && "contact_card" in payload) {
|
|
1548
|
+
const card = contactCardFromDict(
|
|
1549
|
+
payload["contact_card"]
|
|
1550
|
+
);
|
|
1551
|
+
verifyContactCard(card);
|
|
1552
|
+
bindCardToEnvelope(card, envelope, senderVk);
|
|
1553
|
+
}
|
|
1196
1554
|
const senderPkStr = serializeVerifyKey(senderVk);
|
|
1197
1555
|
this._contactBook.addContact(envelope.fromAddress, senderPkStr, {
|
|
1198
1556
|
trustState: "pinned"
|
|
@@ -1249,6 +1607,10 @@ var HandshakeManager = class {
|
|
|
1249
1607
|
}
|
|
1250
1608
|
};
|
|
1251
1609
|
|
|
1610
|
+
// src/sdk/resolver-base.ts
|
|
1611
|
+
var AddressResolver = class {
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1252
1614
|
// src/sdk/tier3.ts
|
|
1253
1615
|
var import_viem = require("viem");
|
|
1254
1616
|
var DEFAULT_RPC_URL = "https://sepolia.base.org";
|
|
@@ -1301,8 +1663,8 @@ var Tier3Resolver = class extends AddressResolver {
|
|
|
1301
1663
|
transport: (0, import_viem.http)(rpcUrl)
|
|
1302
1664
|
});
|
|
1303
1665
|
}
|
|
1304
|
-
async resolvePublicKey(
|
|
1305
|
-
const parsed = parseAddress(
|
|
1666
|
+
async resolvePublicKey(address2, _token, _relayUrl) {
|
|
1667
|
+
const parsed = parseAddress(address2);
|
|
1306
1668
|
const name = parsed.domain;
|
|
1307
1669
|
const cached = this._cache.get(name);
|
|
1308
1670
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -1365,15 +1727,13 @@ var Tier3Resolver = class extends AddressResolver {
|
|
|
1365
1727
|
|
|
1366
1728
|
// src/sdk/resolver.ts
|
|
1367
1729
|
init_dns_verifier();
|
|
1368
|
-
var AddressResolver = class {
|
|
1369
|
-
};
|
|
1370
1730
|
var Tier1Resolver = class extends AddressResolver {
|
|
1371
|
-
async resolvePublicKey(
|
|
1731
|
+
async resolvePublicKey(address2, _token, relayUrl) {
|
|
1372
1732
|
const resp = await fetch(
|
|
1373
|
-
`${relayUrl}/api/v1/agents/${
|
|
1733
|
+
`${relayUrl}/api/v1/agents/${address2}/public-key`
|
|
1374
1734
|
);
|
|
1375
1735
|
if (resp.status === 404) {
|
|
1376
|
-
throw new UAMError(`Agent not found: ${
|
|
1736
|
+
throw new UAMError(`Agent not found: ${address2}`);
|
|
1377
1737
|
}
|
|
1378
1738
|
if (!resp.ok) {
|
|
1379
1739
|
throw new UAMError(
|
|
@@ -1385,8 +1745,8 @@ var Tier1Resolver = class extends AddressResolver {
|
|
|
1385
1745
|
}
|
|
1386
1746
|
};
|
|
1387
1747
|
var Tier2Resolver = class extends AddressResolver {
|
|
1388
|
-
async resolvePublicKey(
|
|
1389
|
-
const parsed = parseAddress(
|
|
1748
|
+
async resolvePublicKey(address2, _token, _relayUrl) {
|
|
1749
|
+
const parsed = parseAddress(address2);
|
|
1390
1750
|
const txtRecords = await queryUamTxt(parsed.domain);
|
|
1391
1751
|
for (const txt of txtRecords) {
|
|
1392
1752
|
const tags = parseUamTxt(txt);
|
|
@@ -1398,37 +1758,37 @@ var Tier2Resolver = class extends AddressResolver {
|
|
|
1398
1758
|
const httpsKey = await resolveKeyViaHttps(parsed.agent, parsed.domain);
|
|
1399
1759
|
if (httpsKey) return httpsKey;
|
|
1400
1760
|
throw new UAMError(
|
|
1401
|
-
`Could not resolve public key for ${
|
|
1761
|
+
`Could not resolve public key for ${address2} via DNS or HTTPS`
|
|
1402
1762
|
);
|
|
1403
1763
|
}
|
|
1404
1764
|
};
|
|
1405
1765
|
var SmartResolver = class extends AddressResolver {
|
|
1406
|
-
|
|
1766
|
+
relayDomain;
|
|
1407
1767
|
_tier1;
|
|
1408
1768
|
_tier2;
|
|
1409
1769
|
_tier3;
|
|
1410
1770
|
constructor(relayDomain, tier3Config) {
|
|
1411
1771
|
super();
|
|
1412
|
-
this.
|
|
1772
|
+
this.relayDomain = relayDomain;
|
|
1413
1773
|
this._tier1 = new Tier1Resolver();
|
|
1414
1774
|
this._tier2 = new Tier2Resolver();
|
|
1415
1775
|
this._tier3 = tier3Config ? new Tier3Resolver(tier3Config) : null;
|
|
1416
1776
|
}
|
|
1417
|
-
async resolvePublicKey(
|
|
1418
|
-
const parsed = parseAddress(
|
|
1419
|
-
const
|
|
1420
|
-
if (
|
|
1421
|
-
return this._tier1.resolvePublicKey(
|
|
1777
|
+
async resolvePublicKey(address2, token, relayUrl) {
|
|
1778
|
+
const parsed = parseAddress(address2);
|
|
1779
|
+
const domain2 = parsed.domain;
|
|
1780
|
+
if (domain2 === this.relayDomain) {
|
|
1781
|
+
return this._tier1.resolvePublicKey(address2, token, relayUrl);
|
|
1422
1782
|
}
|
|
1423
|
-
if (
|
|
1424
|
-
return this._tier2.resolvePublicKey(
|
|
1783
|
+
if (domain2.includes(".")) {
|
|
1784
|
+
return this._tier2.resolvePublicKey(address2, token, relayUrl);
|
|
1425
1785
|
}
|
|
1426
1786
|
if (!this._tier3) {
|
|
1427
1787
|
throw new UAMError(
|
|
1428
|
-
`Tier 3 resolution requires contract config. Cannot resolve dot-free domain: '${
|
|
1788
|
+
`Tier 3 resolution requires contract config. Cannot resolve dot-free domain: '${domain2}'`
|
|
1429
1789
|
);
|
|
1430
1790
|
}
|
|
1431
|
-
return this._tier3.resolvePublicKey(
|
|
1791
|
+
return this._tier3.resolvePublicKey(address2, token, relayUrl);
|
|
1432
1792
|
}
|
|
1433
1793
|
};
|
|
1434
1794
|
|
|
@@ -1437,15 +1797,23 @@ var TransportBase = class {
|
|
|
1437
1797
|
};
|
|
1438
1798
|
|
|
1439
1799
|
// src/sdk/transport/http.ts
|
|
1800
|
+
async function readAnswer(resp) {
|
|
1801
|
+
try {
|
|
1802
|
+
const body = await resp.json();
|
|
1803
|
+
return body !== null && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
1804
|
+
} catch {
|
|
1805
|
+
return {};
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1440
1808
|
var HTTPTransport = class extends TransportBase {
|
|
1441
1809
|
_relayUrl;
|
|
1442
1810
|
_token;
|
|
1443
1811
|
_address;
|
|
1444
|
-
constructor(relayUrl, token,
|
|
1812
|
+
constructor(relayUrl, token, address2) {
|
|
1445
1813
|
super();
|
|
1446
1814
|
this._relayUrl = relayUrl.replace(/\/+$/, "");
|
|
1447
1815
|
this._token = token;
|
|
1448
|
-
this._address =
|
|
1816
|
+
this._address = address2;
|
|
1449
1817
|
}
|
|
1450
1818
|
async connect() {
|
|
1451
1819
|
}
|
|
@@ -1465,6 +1833,7 @@ var HTTPTransport = class extends TransportBase {
|
|
|
1465
1833
|
`HTTP send failed: ${resp.status} ${resp.statusText}`
|
|
1466
1834
|
);
|
|
1467
1835
|
}
|
|
1836
|
+
return readAnswer(resp);
|
|
1468
1837
|
}
|
|
1469
1838
|
async receive(limit = 50) {
|
|
1470
1839
|
const url = new URL(
|
|
@@ -1496,8 +1865,11 @@ var import_ws = __toESM(require("ws"), 1);
|
|
|
1496
1865
|
var BASE_DELAY = 1;
|
|
1497
1866
|
var MAX_DELAY = 60;
|
|
1498
1867
|
var JITTER_RANGE = 1;
|
|
1868
|
+
var SEND_SETTLE_SECONDS = 5;
|
|
1869
|
+
var ACK_TIMEOUT_SECONDS = 5;
|
|
1499
1870
|
var WebSocketTransport = class extends TransportBase {
|
|
1500
1871
|
_url;
|
|
1872
|
+
_token;
|
|
1501
1873
|
_onMessage;
|
|
1502
1874
|
_ws = null;
|
|
1503
1875
|
_pending = [];
|
|
@@ -1506,9 +1878,20 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1506
1878
|
_connectReject = null;
|
|
1507
1879
|
_running = false;
|
|
1508
1880
|
_reconnectTimeout = null;
|
|
1881
|
+
/** Envelopes sent that the relay hasn't answered yet. */
|
|
1882
|
+
_unanswered = 0;
|
|
1883
|
+
_settling = null;
|
|
1884
|
+
_settled = null;
|
|
1885
|
+
/** Sends waiting for the relay's answer, by message_id. */
|
|
1886
|
+
_waiting = /* @__PURE__ */ new Map();
|
|
1887
|
+
/** @internal How long a close waits, in ms. Replaceable in tests. */
|
|
1888
|
+
_settleMs = SEND_SETTLE_SECONDS * 1e3;
|
|
1889
|
+
/** @internal How long send() waits for an answer, in ms. Replaceable in tests. */
|
|
1890
|
+
_ackMs = ACK_TIMEOUT_SECONDS * 1e3;
|
|
1509
1891
|
constructor(wsUrl, token, onMessage) {
|
|
1510
1892
|
super();
|
|
1511
|
-
this._url =
|
|
1893
|
+
this._url = wsUrl;
|
|
1894
|
+
this._token = token;
|
|
1512
1895
|
this._onMessage = onMessage ?? null;
|
|
1513
1896
|
}
|
|
1514
1897
|
async connect() {
|
|
@@ -1529,7 +1912,14 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1529
1912
|
};
|
|
1530
1913
|
});
|
|
1531
1914
|
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Close the connection, once the relay has answered what was sent.
|
|
1917
|
+
*
|
|
1918
|
+
* The relay stops reading a socket when it can't write an ack to it, so
|
|
1919
|
+
* closing straight after send() loses every frame but the first.
|
|
1920
|
+
*/
|
|
1532
1921
|
async disconnect() {
|
|
1922
|
+
await this._allAnswered();
|
|
1533
1923
|
this._running = false;
|
|
1534
1924
|
if (this._reconnectTimeout !== null) {
|
|
1535
1925
|
clearTimeout(this._reconnectTimeout);
|
|
@@ -1541,12 +1931,42 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1541
1931
|
this._ws = null;
|
|
1542
1932
|
}
|
|
1543
1933
|
this._connected = false;
|
|
1934
|
+
this._abandonWaiting();
|
|
1544
1935
|
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Send an envelope and wait for the relay's answer.
|
|
1938
|
+
*
|
|
1939
|
+
* Resolves with the ack that names the message, or null when none came
|
|
1940
|
+
* within _ackMs or the socket went: the message may still have arrived.
|
|
1941
|
+
* Rejects with SendRefusedError when the relay refuses it by message_id
|
|
1942
|
+
* (relays before 1.1 don't name it, so their refusals end as null).
|
|
1943
|
+
*/
|
|
1545
1944
|
async send(envelope) {
|
|
1546
1945
|
if (!this._ws || this._ws.readyState !== import_ws.default.OPEN) {
|
|
1547
1946
|
throw new Error("WebSocket not connected");
|
|
1548
1947
|
}
|
|
1948
|
+
const messageId = envelope["message_id"];
|
|
1949
|
+
let answer = null;
|
|
1950
|
+
let timer;
|
|
1951
|
+
if (typeof messageId === "string" && !this._waiting.has(messageId)) {
|
|
1952
|
+
answer = new Promise((resolve, reject) => {
|
|
1953
|
+
this._waiting.set(messageId, { resolve, reject });
|
|
1954
|
+
timer = setTimeout(() => {
|
|
1955
|
+
log.warn(
|
|
1956
|
+
`No answer from the relay for ${messageId} within ${this._ackMs / 1e3}s`
|
|
1957
|
+
);
|
|
1958
|
+
this._settle(messageId, null);
|
|
1959
|
+
}, this._ackMs);
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1549
1962
|
this._ws.send(JSON.stringify(envelope));
|
|
1963
|
+
this._unanswered += 1;
|
|
1964
|
+
if (answer === null) return null;
|
|
1965
|
+
try {
|
|
1966
|
+
return await answer;
|
|
1967
|
+
} finally {
|
|
1968
|
+
clearTimeout(timer);
|
|
1969
|
+
}
|
|
1550
1970
|
}
|
|
1551
1971
|
async receive(limit = 50) {
|
|
1552
1972
|
const result = this._pending.slice(0, limit);
|
|
@@ -1556,11 +1976,47 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1556
1976
|
async listen(callback) {
|
|
1557
1977
|
this._onMessage = callback;
|
|
1558
1978
|
}
|
|
1979
|
+
/** Resolves when nothing is unanswered, or after _settleMs regardless. */
|
|
1980
|
+
_allAnswered() {
|
|
1981
|
+
if (this._unanswered === 0) return Promise.resolve();
|
|
1982
|
+
this._settling ??= new Promise((resolve) => {
|
|
1983
|
+
const timeout = setTimeout(() => {
|
|
1984
|
+
log.warn(`Closing with ${this._unanswered} sends unanswered`);
|
|
1985
|
+
this._settled?.();
|
|
1986
|
+
}, this._settleMs);
|
|
1987
|
+
this._settled = () => {
|
|
1988
|
+
clearTimeout(timeout);
|
|
1989
|
+
this._settling = null;
|
|
1990
|
+
this._settled = null;
|
|
1991
|
+
resolve();
|
|
1992
|
+
};
|
|
1993
|
+
});
|
|
1994
|
+
return this._settling;
|
|
1995
|
+
}
|
|
1996
|
+
/** Hand the relay's answer (or a refusal) to the send waiting for it. */
|
|
1997
|
+
_settle(messageId, answer, error) {
|
|
1998
|
+
if (typeof messageId !== "string") return;
|
|
1999
|
+
const waiting = this._waiting.get(messageId);
|
|
2000
|
+
if (!waiting) return;
|
|
2001
|
+
this._waiting.delete(messageId);
|
|
2002
|
+
if (error) waiting.reject(error);
|
|
2003
|
+
else waiting.resolve(answer);
|
|
2004
|
+
}
|
|
2005
|
+
/** The socket went: no answers will come for what it carried. */
|
|
2006
|
+
_abandonWaiting() {
|
|
2007
|
+
const waiting = [...this._waiting.values()];
|
|
2008
|
+
this._waiting.clear();
|
|
2009
|
+
for (const w of waiting) w.resolve(null);
|
|
2010
|
+
}
|
|
2011
|
+
_answered() {
|
|
2012
|
+
this._unanswered = Math.max(0, this._unanswered - 1);
|
|
2013
|
+
if (this._unanswered === 0) this._settled?.();
|
|
2014
|
+
}
|
|
1559
2015
|
_startConnectionLoop() {
|
|
1560
2016
|
let attempt = 0;
|
|
1561
2017
|
const tryConnect = () => {
|
|
1562
2018
|
if (!this._running) return;
|
|
1563
|
-
const ws = new import_ws.default(this._url);
|
|
2019
|
+
const ws = new import_ws.default(this._url, [`bearer.${this._token}`]);
|
|
1564
2020
|
this._ws = ws;
|
|
1565
2021
|
ws.on("open", () => {
|
|
1566
2022
|
this._connected = true;
|
|
@@ -1581,6 +2037,9 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1581
2037
|
ws.on("close", () => {
|
|
1582
2038
|
this._connected = false;
|
|
1583
2039
|
this._ws = null;
|
|
2040
|
+
this._unanswered = 0;
|
|
2041
|
+
this._settled?.();
|
|
2042
|
+
this._abandonWaiting();
|
|
1584
2043
|
if (!this._running) return;
|
|
1585
2044
|
attempt += 1;
|
|
1586
2045
|
const delay = Math.min(BASE_DELAY * Math.pow(2, attempt), MAX_DELAY);
|
|
@@ -1597,33 +2056,279 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1597
2056
|
}
|
|
1598
2057
|
_handleMessage(msg) {
|
|
1599
2058
|
const msgType = msg["type"];
|
|
1600
|
-
if (
|
|
1601
|
-
if (this._ws && this._ws.readyState === import_ws.default.OPEN) {
|
|
1602
|
-
this._ws.send(JSON.stringify({ type: "pong" }));
|
|
1603
|
-
}
|
|
1604
|
-
} else if (msgType === "ack") {
|
|
1605
|
-
} else if (msgType === "error" || "error" in msg) {
|
|
1606
|
-
} else if ("uam_version" in msg) {
|
|
2059
|
+
if ("uam_version" in msg) {
|
|
1607
2060
|
if (this._onMessage) {
|
|
1608
2061
|
this._onMessage(msg).catch(() => {
|
|
1609
2062
|
});
|
|
1610
2063
|
} else {
|
|
1611
2064
|
this._pending.push(msg);
|
|
1612
2065
|
}
|
|
2066
|
+
} else if (msgType === "ping") {
|
|
2067
|
+
if (this._ws && this._ws.readyState === import_ws.default.OPEN) {
|
|
2068
|
+
this._ws.send(JSON.stringify({ type: "pong" }));
|
|
2069
|
+
}
|
|
2070
|
+
} else if (msgType === "ack") {
|
|
2071
|
+
this._answered();
|
|
2072
|
+
this._settle(msg["message_id"], msg);
|
|
2073
|
+
} else if (msgType === "relay.notice") {
|
|
2074
|
+
log.warn(
|
|
2075
|
+
`Relay notice: message ${String(msg["message_id"])} is ${String(msg["status"])}`
|
|
2076
|
+
);
|
|
2077
|
+
} else if (msgType === "error" || "error" in msg) {
|
|
2078
|
+
this._answered();
|
|
2079
|
+
const code = String(msg["error"] ?? msg["code"] ?? "error");
|
|
2080
|
+
const detail = typeof msg["detail"] === "string" ? msg["detail"] : void 0;
|
|
2081
|
+
log.warn(`Relay error: [${code}] ${detail ?? ""}`.trimEnd());
|
|
2082
|
+
const messageId = msg["message_id"];
|
|
2083
|
+
if (typeof messageId === "string") {
|
|
2084
|
+
this._settle(messageId, null, new SendRefusedError(messageId, code, detail));
|
|
2085
|
+
}
|
|
1613
2086
|
}
|
|
1614
2087
|
}
|
|
1615
2088
|
};
|
|
1616
2089
|
|
|
1617
2090
|
// src/sdk/transport/index.ts
|
|
1618
|
-
function createTransport(config, token,
|
|
2091
|
+
function createTransport(config, token, address2, onMessage) {
|
|
1619
2092
|
if (config.transportType === "http") {
|
|
1620
|
-
return new HTTPTransport(config.relayUrl, token,
|
|
2093
|
+
return new HTTPTransport(config.relayUrl, token, address2);
|
|
1621
2094
|
} else {
|
|
1622
2095
|
return new WebSocketTransport(config.relayWsUrl, token, onMessage);
|
|
1623
2096
|
}
|
|
1624
2097
|
}
|
|
1625
2098
|
|
|
2099
|
+
// src/sdk/replay-cache.ts
|
|
2100
|
+
var import_node_fs4 = require("fs");
|
|
2101
|
+
var import_node_path4 = require("path");
|
|
2102
|
+
var import_promises2 = require("timers/promises");
|
|
2103
|
+
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
2104
|
+
var RETENTION_SECONDS = STORED_MAIL_MAX_AGE + 2 * MAX_ENVELOPE_AGE;
|
|
2105
|
+
var PURGE_INTERVAL_MS = 6e4;
|
|
2106
|
+
var WAL_ATTEMPTS = 100;
|
|
2107
|
+
var WAL_RETRY_MS = 100;
|
|
2108
|
+
var SCHEMA = `
|
|
2109
|
+
CREATE TABLE IF NOT EXISTS seen_envelopes (
|
|
2110
|
+
from_address TEXT NOT NULL,
|
|
2111
|
+
message_id TEXT NOT NULL,
|
|
2112
|
+
seen_at REAL NOT NULL,
|
|
2113
|
+
PRIMARY KEY (from_address, message_id)
|
|
2114
|
+
) WITHOUT ROWID;
|
|
2115
|
+
CREATE INDEX IF NOT EXISTS seen_envelopes_seen_at ON seen_envelopes (seen_at);
|
|
2116
|
+
CREATE TABLE IF NOT EXISTS replay_meta (
|
|
2117
|
+
key TEXT PRIMARY KEY,
|
|
2118
|
+
value TEXT NOT NULL
|
|
2119
|
+
);
|
|
2120
|
+
`;
|
|
2121
|
+
function isLocked(err) {
|
|
2122
|
+
const code = err.code;
|
|
2123
|
+
return typeof code === "string" && (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_LOCKED"));
|
|
2124
|
+
}
|
|
2125
|
+
async function enableWal(db) {
|
|
2126
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
2127
|
+
try {
|
|
2128
|
+
db.pragma("journal_mode = WAL");
|
|
2129
|
+
return;
|
|
2130
|
+
} catch (err) {
|
|
2131
|
+
if (attempt >= WAL_ATTEMPTS || !isLocked(err)) throw err;
|
|
2132
|
+
await (0, import_promises2.setTimeout)(WAL_RETRY_MS);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
var DEFAULT_REPLAY_CACHE_CAPACITY = 1e6;
|
|
2137
|
+
var EnvelopeReplayCache = class {
|
|
2138
|
+
capacity;
|
|
2139
|
+
ttlMs;
|
|
2140
|
+
_now;
|
|
2141
|
+
// Map iteration order is insertion order: first key = least recently used.
|
|
2142
|
+
_seen = /* @__PURE__ */ new Map();
|
|
2143
|
+
constructor(options) {
|
|
2144
|
+
const capacity = options?.capacity ?? 1e5;
|
|
2145
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
2146
|
+
throw new RangeError(
|
|
2147
|
+
`Replay cache capacity must be a positive integer, got ${capacity}`
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
this.capacity = capacity;
|
|
2151
|
+
this.ttlMs = options?.ttlMs ?? 3e5;
|
|
2152
|
+
this._now = options?.now ?? (() => Date.now());
|
|
2153
|
+
}
|
|
2154
|
+
get size() {
|
|
2155
|
+
return this._seen.size;
|
|
2156
|
+
}
|
|
2157
|
+
/** True if this pair was seen within the TTL. Records nothing. */
|
|
2158
|
+
has(fromAddress, messageId) {
|
|
2159
|
+
const seenAt = this._seen.get(this._key(fromAddress, messageId));
|
|
2160
|
+
return seenAt !== void 0 && this._now() - seenAt < this.ttlMs;
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Return true if this (fromAddress, messageId) pair was already seen
|
|
2164
|
+
* within the TTL (a replay). Otherwise record it and return false.
|
|
2165
|
+
*/
|
|
2166
|
+
seenOrRecord(fromAddress, messageId) {
|
|
2167
|
+
const key = this._key(fromAddress, messageId);
|
|
2168
|
+
const now = this._now();
|
|
2169
|
+
const seenAt = this._seen.get(key);
|
|
2170
|
+
if (seenAt !== void 0 && now - seenAt < this.ttlMs) {
|
|
2171
|
+
this._seen.delete(key);
|
|
2172
|
+
this._seen.set(key, seenAt);
|
|
2173
|
+
return true;
|
|
2174
|
+
}
|
|
2175
|
+
this._seen.delete(key);
|
|
2176
|
+
this._seen.set(key, now);
|
|
2177
|
+
while (this._seen.size > this.capacity) {
|
|
2178
|
+
const oldest = this._seen.keys().next().value;
|
|
2179
|
+
this._seen.delete(oldest);
|
|
2180
|
+
}
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
_key(fromAddress, messageId) {
|
|
2184
|
+
return JSON.stringify([fromAddress, messageId]);
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
var PersistentReplayCache = class {
|
|
2188
|
+
capacity;
|
|
2189
|
+
_path;
|
|
2190
|
+
_now;
|
|
2191
|
+
_db = null;
|
|
2192
|
+
_opening = null;
|
|
2193
|
+
_count = 0;
|
|
2194
|
+
_lastPurge = 0;
|
|
2195
|
+
_createdAt = 0;
|
|
2196
|
+
// Used only while the file is full, for envelopes inside MAX_ENVELOPE_AGE.
|
|
2197
|
+
// One can be signed that far ahead of us and stay acceptable until that far
|
|
2198
|
+
// behind, so its id must last both.
|
|
2199
|
+
_overflow;
|
|
2200
|
+
/**
|
|
2201
|
+
* @param path - The SQLite file
|
|
2202
|
+
* @param options.now - The clock, in ms since the epoch
|
|
2203
|
+
*/
|
|
2204
|
+
constructor(path, options) {
|
|
2205
|
+
const capacity = options?.capacity ?? DEFAULT_REPLAY_CACHE_CAPACITY;
|
|
2206
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
2207
|
+
throw new RangeError(
|
|
2208
|
+
`Replay cache capacity must be a positive integer, got ${capacity}`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
this.capacity = capacity;
|
|
2212
|
+
this._path = path;
|
|
2213
|
+
this._now = options?.now ?? (() => Date.now());
|
|
2214
|
+
this._overflow = new EnvelopeReplayCache({
|
|
2215
|
+
capacity: 1e4,
|
|
2216
|
+
ttlMs: 2 * MAX_ENVELOPE_AGE * 1e3,
|
|
2217
|
+
now: this._now
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
/** Open the file now, rather than on first use. */
|
|
2221
|
+
async open() {
|
|
2222
|
+
await this._open();
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Return true if the envelope must be dropped as a replay. Otherwise record
|
|
2226
|
+
* the pair and return false.
|
|
2227
|
+
*
|
|
2228
|
+
* @param signedAt - The envelope's signed time, in ms since the epoch. It
|
|
2229
|
+
* matters only when the cache is full.
|
|
2230
|
+
*/
|
|
2231
|
+
async seenOrRecord(fromAddress, messageId, signedAt) {
|
|
2232
|
+
const db = await this._open();
|
|
2233
|
+
const now = this._now();
|
|
2234
|
+
if (now - this._lastPurge >= PURGE_INTERVAL_MS) this._purge(db);
|
|
2235
|
+
if (this._overflow.has(fromAddress, messageId)) return true;
|
|
2236
|
+
if (this._count < this.capacity) {
|
|
2237
|
+
const { changes } = db.prepare(
|
|
2238
|
+
"INSERT OR IGNORE INTO seen_envelopes (from_address, message_id, seen_at) VALUES (?, ?, ?)"
|
|
2239
|
+
).run(fromAddress, messageId, now / 1e3);
|
|
2240
|
+
if (changes === 0) return true;
|
|
2241
|
+
this._count += 1;
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
2244
|
+
const known = db.prepare(
|
|
2245
|
+
"SELECT 1 FROM seen_envelopes WHERE from_address = ? AND message_id = ?"
|
|
2246
|
+
).get(fromAddress, messageId);
|
|
2247
|
+
if (known !== void 0) return true;
|
|
2248
|
+
if (Math.abs(now - signedAt) <= MAX_ENVELOPE_AGE * 1e3) {
|
|
2249
|
+
return this._overflow.seenOrRecord(fromAddress, messageId);
|
|
2250
|
+
}
|
|
2251
|
+
log.warn(
|
|
2252
|
+
`Replay cache full (${this._count} ids); refused envelope ${messageId} from ${fromAddress} signed ${Math.round((now - signedAt) / 1e3)}s ago`
|
|
2253
|
+
);
|
|
2254
|
+
return true;
|
|
2255
|
+
}
|
|
2256
|
+
/** When this cache file was first created, in ms since the epoch. */
|
|
2257
|
+
async createdAt() {
|
|
2258
|
+
await this._open();
|
|
2259
|
+
return this._createdAt;
|
|
2260
|
+
}
|
|
2261
|
+
/** True if an envelope signed at `signedAt` (ms) predates this cache. */
|
|
2262
|
+
async signedBeforeCache(signedAt) {
|
|
2263
|
+
return signedAt < await this.createdAt();
|
|
2264
|
+
}
|
|
2265
|
+
/** Number of ids currently held on disk. */
|
|
2266
|
+
async count() {
|
|
2267
|
+
const db = await this._open();
|
|
2268
|
+
return this._countRows(db);
|
|
2269
|
+
}
|
|
2270
|
+
/** Close the file; the next call opens it again. */
|
|
2271
|
+
async close() {
|
|
2272
|
+
await this._opening?.catch(() => void 0);
|
|
2273
|
+
this._db?.close();
|
|
2274
|
+
this._db = null;
|
|
2275
|
+
}
|
|
2276
|
+
_open() {
|
|
2277
|
+
if (this._db !== null) return Promise.resolve(this._db);
|
|
2278
|
+
this._opening ??= this._connect().finally(() => {
|
|
2279
|
+
this._opening = null;
|
|
2280
|
+
});
|
|
2281
|
+
return this._opening;
|
|
2282
|
+
}
|
|
2283
|
+
async _connect() {
|
|
2284
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(this._path), { recursive: true });
|
|
2285
|
+
const db = new import_better_sqlite32.default(this._path, { timeout: 3e4 });
|
|
2286
|
+
try {
|
|
2287
|
+
await enableWal(db);
|
|
2288
|
+
db.exec(SCHEMA);
|
|
2289
|
+
db.prepare(
|
|
2290
|
+
"INSERT OR IGNORE INTO replay_meta (key, value) VALUES ('created_at', ?)"
|
|
2291
|
+
).run(String(this._now() / 1e3));
|
|
2292
|
+
const row = db.prepare("SELECT value FROM replay_meta WHERE key = 'created_at'").get();
|
|
2293
|
+
this._createdAt = Number(row.value) * 1e3;
|
|
2294
|
+
this._purge(db);
|
|
2295
|
+
} catch (err) {
|
|
2296
|
+
db.close();
|
|
2297
|
+
throw err;
|
|
2298
|
+
}
|
|
2299
|
+
this._db = db;
|
|
2300
|
+
return db;
|
|
2301
|
+
}
|
|
2302
|
+
_purge(db) {
|
|
2303
|
+
const now = this._now();
|
|
2304
|
+
db.prepare("DELETE FROM seen_envelopes WHERE seen_at < ?").run(
|
|
2305
|
+
now / 1e3 - RETENTION_SECONDS
|
|
2306
|
+
);
|
|
2307
|
+
this._count = this._countRows(db);
|
|
2308
|
+
this._lastPurge = now;
|
|
2309
|
+
}
|
|
2310
|
+
_countRows(db) {
|
|
2311
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM seen_envelopes").get();
|
|
2312
|
+
return row.n;
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
|
|
1626
2316
|
// src/sdk/agent.ts
|
|
2317
|
+
var RELAY_NO_MEMORY_MS = 6e4;
|
|
2318
|
+
function replayCacheCapacityFromEnv() {
|
|
2319
|
+
const raw = process.env["UAM_REPLAY_CACHE_CAPACITY"];
|
|
2320
|
+
if (raw === void 0 || raw === "") {
|
|
2321
|
+
return DEFAULT_REPLAY_CACHE_CAPACITY;
|
|
2322
|
+
}
|
|
2323
|
+
const n = Number(raw);
|
|
2324
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
2325
|
+
log.warn(
|
|
2326
|
+
`Ignoring invalid UAM_REPLAY_CACHE_CAPACITY=${JSON.stringify(raw)}; using ${DEFAULT_REPLAY_CACHE_CAPACITY}`
|
|
2327
|
+
);
|
|
2328
|
+
return DEFAULT_REPLAY_CACHE_CAPACITY;
|
|
2329
|
+
}
|
|
2330
|
+
return n;
|
|
2331
|
+
}
|
|
1627
2332
|
var Agent = class {
|
|
1628
2333
|
/** @internal */
|
|
1629
2334
|
_config;
|
|
@@ -1640,6 +2345,23 @@ var Agent = class {
|
|
|
1640
2345
|
_contactBook;
|
|
1641
2346
|
/** @internal */
|
|
1642
2347
|
_handshake;
|
|
2348
|
+
/**
|
|
2349
|
+
* @internal Wall clock in ms since the epoch, used by the inbound
|
|
2350
|
+
* freshness/expiry checks and the replay cache. Replaceable in tests.
|
|
2351
|
+
*/
|
|
2352
|
+
_clock = () => Date.now();
|
|
2353
|
+
/**
|
|
2354
|
+
* @internal Inbound replay cache, keyed by (from address, message id).
|
|
2355
|
+
* Kept on disk under the data dir, so it outlives restarts and is shared by
|
|
2356
|
+
* every process using it; capacity from UAM_REPLAY_CACHE_CAPACITY.
|
|
2357
|
+
*/
|
|
2358
|
+
_replayCache;
|
|
2359
|
+
/** Whether our relay has said it writes relay_meta. */
|
|
2360
|
+
_relayStamps = false;
|
|
2361
|
+
/** Until when, by _monotonic, to take the relay's last no as still true. */
|
|
2362
|
+
_relayNoUntil = 0;
|
|
2363
|
+
/** @internal Monotonic clock in ms. Replaceable in tests. */
|
|
2364
|
+
_monotonic = () => performance.now();
|
|
1643
2365
|
/**
|
|
1644
2366
|
* Create an Agent. No I/O happens here -- call connect() to initialize.
|
|
1645
2367
|
*/
|
|
@@ -1661,6 +2383,13 @@ var Agent = class {
|
|
|
1661
2383
|
this._contactBook,
|
|
1662
2384
|
this._config.trustPolicy
|
|
1663
2385
|
);
|
|
2386
|
+
this._replayCache = new PersistentReplayCache(
|
|
2387
|
+
(0, import_node_path5.join)(this._config.dataDir, "replay", "replay.db"),
|
|
2388
|
+
{
|
|
2389
|
+
capacity: replayCacheCapacityFromEnv(),
|
|
2390
|
+
now: () => this._clock()
|
|
2391
|
+
}
|
|
2392
|
+
);
|
|
1664
2393
|
}
|
|
1665
2394
|
// -- Properties ----------------------------------------------------------
|
|
1666
2395
|
/** The agent's full UAM address (e.g., 'myagent::youam.network'). */
|
|
@@ -1674,7 +2403,7 @@ var Agent = class {
|
|
|
1674
2403
|
}
|
|
1675
2404
|
/** UAM SDK version. */
|
|
1676
2405
|
get version() {
|
|
1677
|
-
return
|
|
2406
|
+
return SDK_VERSION;
|
|
1678
2407
|
}
|
|
1679
2408
|
/** The agent's public key (base64-encoded Ed25519 verify key). */
|
|
1680
2409
|
get publicKey() {
|
|
@@ -1711,10 +2440,19 @@ var Agent = class {
|
|
|
1711
2440
|
}
|
|
1712
2441
|
await sodiumReady;
|
|
1713
2442
|
this._keyManager.loadOrGenerate(this._config.name);
|
|
1714
|
-
|
|
2443
|
+
try {
|
|
2444
|
+
await this._replayCache.open();
|
|
2445
|
+
} catch (err) {
|
|
2446
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2447
|
+
log.warn(`Could not open the replay cache: ${reason}`);
|
|
2448
|
+
}
|
|
2449
|
+
const storedToken = this._keyManager.loadToken(
|
|
2450
|
+
this._config.name,
|
|
2451
|
+
this._config.relayUrl
|
|
2452
|
+
);
|
|
1715
2453
|
if (storedToken) {
|
|
1716
2454
|
this._token = storedToken;
|
|
1717
|
-
this.
|
|
2455
|
+
this._useAddress(await this._storedAddress());
|
|
1718
2456
|
} else if (this._autoRegister) {
|
|
1719
2457
|
await this._registerWithRelay();
|
|
1720
2458
|
} else {
|
|
@@ -1737,6 +2475,7 @@ var Agent = class {
|
|
|
1737
2475
|
*/
|
|
1738
2476
|
async close() {
|
|
1739
2477
|
this._contactBook.close();
|
|
2478
|
+
await this._replayCache.close();
|
|
1740
2479
|
if (this._transport) {
|
|
1741
2480
|
await this._transport.disconnect();
|
|
1742
2481
|
}
|
|
@@ -1744,7 +2483,12 @@ var Agent = class {
|
|
|
1744
2483
|
}
|
|
1745
2484
|
// -- Messaging -----------------------------------------------------------
|
|
1746
2485
|
/**
|
|
1747
|
-
* Send an encrypted, signed message.
|
|
2486
|
+
* Send an encrypted, signed message.
|
|
2487
|
+
*
|
|
2488
|
+
* Returns the message id with what the relay did with it: `status`
|
|
2489
|
+
* "pending" means the recipient only takes approved contacts, and the
|
|
2490
|
+
* message was not kept (don't resend it). `delivered` is true only when
|
|
2491
|
+
* the relay says the recipient has it.
|
|
1748
2492
|
*/
|
|
1749
2493
|
async send(toAddress, message, options) {
|
|
1750
2494
|
await this._ensureConnected();
|
|
@@ -1757,7 +2501,8 @@ var Agent = class {
|
|
|
1757
2501
|
}
|
|
1758
2502
|
}
|
|
1759
2503
|
const recipientVk = await this._resolvePublicKey(toAddress);
|
|
1760
|
-
|
|
2504
|
+
const trustState = this._contactBook.getTrustState(toAddress);
|
|
2505
|
+
if (trustState !== "pinned" && trustState !== "verified" && trustState !== "trusted" && trustState !== "handshake-sent") {
|
|
1761
2506
|
await this._initiateHandshake(toAddress, recipientVk);
|
|
1762
2507
|
}
|
|
1763
2508
|
const envelope = await createEnvelope(
|
|
@@ -1775,20 +2520,29 @@ var Agent = class {
|
|
|
1775
2520
|
);
|
|
1776
2521
|
const wire = toWireDict(envelope);
|
|
1777
2522
|
const relayUrls = await this._getRelayUrls(toAddress);
|
|
2523
|
+
let answer;
|
|
1778
2524
|
if (relayUrls !== null && relayUrls.length === 1 && relayUrls[0] === this._config.relayUrl) {
|
|
1779
|
-
await this._transport.send(wire);
|
|
2525
|
+
answer = await this._transport.send(wire);
|
|
1780
2526
|
} else if (relayUrls !== null && relayUrls.length > 0) {
|
|
1781
|
-
await this._trySendWithFailover(wire, relayUrls);
|
|
2527
|
+
answer = await this._trySendWithFailover(wire, relayUrls);
|
|
1782
2528
|
} else {
|
|
1783
|
-
await this._transport.send(wire);
|
|
2529
|
+
answer = await this._transport.send(wire);
|
|
1784
2530
|
}
|
|
1785
|
-
|
|
2531
|
+
const status = sendStatus(answer);
|
|
2532
|
+
return {
|
|
2533
|
+
messageId: envelope.messageId,
|
|
2534
|
+
status,
|
|
2535
|
+
delivered: status === "delivered"
|
|
2536
|
+
};
|
|
1786
2537
|
}
|
|
1787
2538
|
/**
|
|
1788
2539
|
* Retrieve, decrypt, and verify pending messages.
|
|
1789
2540
|
*
|
|
1790
2541
|
* Returns a list of ReceivedMessage data objects.
|
|
1791
|
-
*
|
|
2542
|
+
* Drops messages with invalid signatures or decryption errors. A message
|
|
2543
|
+
* that fails processing for any other reason (malformed envelope, bad
|
|
2544
|
+
* handshake payload, ...) is logged and dropped; it never aborts the
|
|
2545
|
+
* rest of the batch.
|
|
1792
2546
|
*/
|
|
1793
2547
|
async inbox(limit = 50) {
|
|
1794
2548
|
await this._ensureConnected();
|
|
@@ -1796,7 +2550,15 @@ var Agent = class {
|
|
|
1796
2550
|
const rawMessages = await this._transport.receive(limit);
|
|
1797
2551
|
const result = [];
|
|
1798
2552
|
for (const raw of rawMessages) {
|
|
1799
|
-
|
|
2553
|
+
let msg;
|
|
2554
|
+
try {
|
|
2555
|
+
msg = await this._processInbound(raw);
|
|
2556
|
+
} catch (err) {
|
|
2557
|
+
log.warn(
|
|
2558
|
+
`Dropping inbound message ${String(raw["message_id"])} from ${String(raw["from"])}: ${err instanceof Error ? err.message : String(err)}`
|
|
2559
|
+
);
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
1800
2562
|
if (msg !== null) {
|
|
1801
2563
|
result.push(msg);
|
|
1802
2564
|
await this._sendReadReceipt(msg);
|
|
@@ -1816,42 +2578,43 @@ var Agent = class {
|
|
|
1816
2578
|
* Adds the sender to contacts with trust_source='explicit-approval'
|
|
1817
2579
|
* and sends handshake.accept back.
|
|
1818
2580
|
*/
|
|
1819
|
-
async approve(
|
|
2581
|
+
async approve(address2) {
|
|
1820
2582
|
await this._ensureConnected();
|
|
1821
2583
|
const pendingList = this._contactBook.getPending();
|
|
1822
|
-
const entry = pendingList.find((p) => p.address ===
|
|
2584
|
+
const entry = pendingList.find((p) => p.address === address2);
|
|
1823
2585
|
if (!entry) {
|
|
1824
|
-
throw new UAMError(`No pending handshake from ${
|
|
2586
|
+
throw new UAMError(`No pending handshake from ${address2}`);
|
|
1825
2587
|
}
|
|
1826
2588
|
const cardDict = JSON.parse(entry.contactCard);
|
|
1827
2589
|
const card = contactCardFromDict(cardDict);
|
|
1828
2590
|
verifyContactCard(card);
|
|
2591
|
+
const senderVk = deserializeVerifyKey(card.publicKey);
|
|
2592
|
+
bindCardToEnvelope(card, { fromAddress: address2 }, senderVk);
|
|
1829
2593
|
this._contactBook.addContact(card.address, card.publicKey, {
|
|
1830
2594
|
displayName: card.displayName,
|
|
1831
2595
|
trustState: "trusted",
|
|
1832
2596
|
trustSource: "explicit-approval"
|
|
1833
2597
|
});
|
|
1834
|
-
this._contactBook.removePending(
|
|
1835
|
-
|
|
1836
|
-
await this._handshake._sendAccept(this, address, senderVk);
|
|
2598
|
+
this._contactBook.removePending(address2);
|
|
2599
|
+
await this._handshake._sendAccept(this, address2, senderVk);
|
|
1837
2600
|
}
|
|
1838
2601
|
/**
|
|
1839
2602
|
* Deny a pending handshake request.
|
|
1840
2603
|
*
|
|
1841
2604
|
* Removes the entry from pending and sends handshake.deny.
|
|
1842
2605
|
*/
|
|
1843
|
-
async deny(
|
|
2606
|
+
async deny(address2) {
|
|
1844
2607
|
await this._ensureConnected();
|
|
1845
2608
|
const pendingList = this._contactBook.getPending();
|
|
1846
|
-
const entry = pendingList.find((p) => p.address ===
|
|
2609
|
+
const entry = pendingList.find((p) => p.address === address2);
|
|
1847
2610
|
if (!entry) {
|
|
1848
|
-
throw new UAMError(`No pending handshake from ${
|
|
2611
|
+
throw new UAMError(`No pending handshake from ${address2}`);
|
|
1849
2612
|
}
|
|
1850
2613
|
const cardDict = JSON.parse(entry.contactCard);
|
|
1851
2614
|
const card = contactCardFromDict(cardDict);
|
|
1852
2615
|
const senderVk = deserializeVerifyKey(card.publicKey);
|
|
1853
|
-
this._contactBook.removePending(
|
|
1854
|
-
await this._handshake._sendDeny(this,
|
|
2616
|
+
this._contactBook.removePending(address2);
|
|
2617
|
+
await this._handshake._sendDeny(this, address2, senderVk);
|
|
1855
2618
|
}
|
|
1856
2619
|
/**
|
|
1857
2620
|
* Block an address or domain pattern (HAND-04).
|
|
@@ -1877,7 +2640,7 @@ var Agent = class {
|
|
|
1877
2640
|
*
|
|
1878
2641
|
* Returns `true` if verified, `false` on timeout.
|
|
1879
2642
|
*/
|
|
1880
|
-
async verifyDomain(
|
|
2643
|
+
async verifyDomain(domain2, options) {
|
|
1881
2644
|
const { generateTxtRecord: generateTxtRecord2 } = await Promise.resolve().then(() => (init_dns_verifier(), dns_verifier_exports));
|
|
1882
2645
|
await this._ensureConnected();
|
|
1883
2646
|
const timeout = options?.timeout ?? 3e5;
|
|
@@ -1898,7 +2661,7 @@ var Agent = class {
|
|
|
1898
2661
|
"Content-Type": "application/json",
|
|
1899
2662
|
Authorization: `Bearer ${this._token}`
|
|
1900
2663
|
},
|
|
1901
|
-
body: JSON.stringify({ domain }),
|
|
2664
|
+
body: JSON.stringify({ domain: domain2 }),
|
|
1902
2665
|
signal: AbortSignal.timeout(3e4)
|
|
1903
2666
|
}
|
|
1904
2667
|
);
|
|
@@ -1951,7 +2714,7 @@ var Agent = class {
|
|
|
1951
2714
|
if (!resp.ok) {
|
|
1952
2715
|
throw new Error(`HTTP ${resp.status}`);
|
|
1953
2716
|
}
|
|
1954
|
-
return;
|
|
2717
|
+
return await readAnswer(resp);
|
|
1955
2718
|
} catch (err) {
|
|
1956
2719
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1957
2720
|
}
|
|
@@ -1969,6 +2732,10 @@ var Agent = class {
|
|
|
1969
2732
|
/**
|
|
1970
2733
|
* Resolve a recipient's public key, checking contact book first.
|
|
1971
2734
|
*
|
|
2735
|
+
* Pure lookup: never writes to the contact book. The contact row is
|
|
2736
|
+
* created by _initiateHandshake (as "handshake-sent") once the request
|
|
2737
|
+
* has gone out, giving the lifecycle no row -> handshake-sent -> pinned.
|
|
2738
|
+
*
|
|
1972
2739
|
* TOFU-01: Known contacts always use stored key (zero network I/O).
|
|
1973
2740
|
* TOFU-03: Race-condition double-check after network resolve.
|
|
1974
2741
|
* Raises KeyPinningError if a pinned contact's key doesn't match.
|
|
@@ -1992,9 +2759,6 @@ var Agent = class {
|
|
|
1992
2759
|
);
|
|
1993
2760
|
}
|
|
1994
2761
|
}
|
|
1995
|
-
this._contactBook.addContact(toAddress, resolvedPkStr, {
|
|
1996
|
-
trustState: "provisional"
|
|
1997
|
-
});
|
|
1998
2762
|
return deserializeVerifyKey(resolvedPkStr);
|
|
1999
2763
|
}
|
|
2000
2764
|
async _initiateHandshake(toAddress, recipientVk) {
|
|
@@ -2041,6 +2805,9 @@ var Agent = class {
|
|
|
2041
2805
|
}
|
|
2042
2806
|
throw err;
|
|
2043
2807
|
}
|
|
2808
|
+
if (!await this._passesInboundChecks(envelope, raw)) {
|
|
2809
|
+
return null;
|
|
2810
|
+
}
|
|
2044
2811
|
if (envelope.type === "handshake.request" /* HANDSHAKE_REQUEST */ || envelope.type === "handshake.accept" /* HANDSHAKE_ACCEPT */ || envelope.type === "handshake.deny" /* HANDSHAKE_DENY */) {
|
|
2045
2812
|
return this._handshake.handleInbound(this, envelope, senderVk);
|
|
2046
2813
|
}
|
|
@@ -2075,6 +2842,102 @@ var Agent = class {
|
|
|
2075
2842
|
verified: true
|
|
2076
2843
|
});
|
|
2077
2844
|
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Recipient, freshness, expiry and replay checks for a signature-verified
|
|
2847
|
+
* inbound envelope. Returns false (and logs) when it must be dropped.
|
|
2848
|
+
* Matches the Python SDK's _process_inbound checks.
|
|
2849
|
+
*/
|
|
2850
|
+
async _passesInboundChecks(envelope, raw) {
|
|
2851
|
+
const id = envelope.messageId;
|
|
2852
|
+
const from = envelope.fromAddress;
|
|
2853
|
+
if (this._address !== null && envelope.toAddress !== this._address) {
|
|
2854
|
+
log.warn(
|
|
2855
|
+
`Dropped envelope ${id} from ${from} addressed to ${envelope.toAddress} (we are ${this._address})`
|
|
2856
|
+
);
|
|
2857
|
+
return false;
|
|
2858
|
+
}
|
|
2859
|
+
const now = this._clock();
|
|
2860
|
+
const ts = Date.parse(envelope.timestamp);
|
|
2861
|
+
if (Number.isNaN(ts)) {
|
|
2862
|
+
log.warn(
|
|
2863
|
+
`Dropped envelope ${id} from ${from} with malformed timestamp ` + JSON.stringify(envelope.timestamp)
|
|
2864
|
+
);
|
|
2865
|
+
return false;
|
|
2866
|
+
}
|
|
2867
|
+
const ageSeconds = Math.abs(now - ts) / 1e3;
|
|
2868
|
+
if (ageSeconds > MAX_ENVELOPE_AGE && !await this._wasStored(raw, envelope.toAddress, ts, now)) {
|
|
2869
|
+
log.info(
|
|
2870
|
+
`Dropped stale envelope ${id} from ${from} (age=${Math.round(ageSeconds)}s > ${MAX_ENVELOPE_AGE}s)`
|
|
2871
|
+
);
|
|
2872
|
+
return false;
|
|
2873
|
+
}
|
|
2874
|
+
if (envelope.expires !== void 0 && envelope.expires !== null) {
|
|
2875
|
+
const exp = Date.parse(envelope.expires);
|
|
2876
|
+
if (Number.isNaN(exp)) {
|
|
2877
|
+
log.warn(
|
|
2878
|
+
`Dropped envelope ${id} from ${from} with malformed expires ` + JSON.stringify(envelope.expires)
|
|
2879
|
+
);
|
|
2880
|
+
return false;
|
|
2881
|
+
}
|
|
2882
|
+
if (now > exp) {
|
|
2883
|
+
log.info(
|
|
2884
|
+
`Dropped expired envelope ${id} from ${from} (expires=${envelope.expires})`
|
|
2885
|
+
);
|
|
2886
|
+
return false;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
if (await this._replayCache.seenOrRecord(from, id, ts)) {
|
|
2890
|
+
log.info(`Dropped replayed envelope ${id} from ${from}`);
|
|
2891
|
+
return false;
|
|
2892
|
+
}
|
|
2893
|
+
return true;
|
|
2894
|
+
}
|
|
2895
|
+
/** True if a message too old to be live is stored mail, not a replay. */
|
|
2896
|
+
async _wasStored(raw, toAddress, signedAt, now) {
|
|
2897
|
+
const relayDomain = toAddress.split("::").pop();
|
|
2898
|
+
if (!relayVouches(raw, signedAt, now, relayDomain)) return false;
|
|
2899
|
+
if (await this._replayCache.signedBeforeCache(signedAt)) return false;
|
|
2900
|
+
return this._relayStampsMail();
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* True if our relay writes relay_meta, so a sender can't have.
|
|
2904
|
+
*
|
|
2905
|
+
* Asked only when a stale message carries a stamp. A yes is remembered for
|
|
2906
|
+
* good, a no for RELAY_NO_MEMORY_MS: a relay can be upgraded while this
|
|
2907
|
+
* agent runs. A failed lookup isn't remembered, because the relay has
|
|
2908
|
+
* already marked this mail delivered and the next message should get a
|
|
2909
|
+
* real answer.
|
|
2910
|
+
*/
|
|
2911
|
+
async _relayStampsMail() {
|
|
2912
|
+
if (this._relayStamps) return true;
|
|
2913
|
+
if (this._monotonic() < this._relayNoUntil) return false;
|
|
2914
|
+
try {
|
|
2915
|
+
const features = await this._fetchRelayFeatures();
|
|
2916
|
+
this._relayStamps = features.includes(RELAY_META_FEATURE);
|
|
2917
|
+
if (!this._relayStamps) {
|
|
2918
|
+
this._relayNoUntil = this._monotonic() + RELAY_NO_MEMORY_MS;
|
|
2919
|
+
}
|
|
2920
|
+
} catch (err) {
|
|
2921
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2922
|
+
log.warn(`Cannot read relay features: ${reason}`);
|
|
2923
|
+
}
|
|
2924
|
+
return this._relayStamps;
|
|
2925
|
+
}
|
|
2926
|
+
/** The relay's features; a body that doesn't list any is an empty list. */
|
|
2927
|
+
async _fetchRelayFeatures() {
|
|
2928
|
+
const resp = await fetch(`${this._config.relayUrl}/api/v1/versions`, {
|
|
2929
|
+
signal: AbortSignal.timeout(1e4)
|
|
2930
|
+
});
|
|
2931
|
+
if (!resp.ok) throw new UAMError(`Relay answered ${resp.status}`);
|
|
2932
|
+
const body = await resp.text();
|
|
2933
|
+
let data;
|
|
2934
|
+
try {
|
|
2935
|
+
data = JSON.parse(body);
|
|
2936
|
+
} catch {
|
|
2937
|
+
return [];
|
|
2938
|
+
}
|
|
2939
|
+
return Array.isArray(data?.features) ? data.features : [];
|
|
2940
|
+
}
|
|
2078
2941
|
/**
|
|
2079
2942
|
* Sweep expired pending handshakes and send receipt.failed (HAND-03).
|
|
2080
2943
|
*/
|
|
@@ -2127,7 +2990,8 @@ var Agent = class {
|
|
|
2127
2990
|
"receipt.read" /* RECEIPT_READ */,
|
|
2128
2991
|
new TextEncoder().encode(receiptPayload),
|
|
2129
2992
|
this._keyManager.signingKey,
|
|
2130
|
-
senderVk
|
|
2993
|
+
senderVk,
|
|
2994
|
+
{ replyTo: msg.messageId }
|
|
2131
2995
|
);
|
|
2132
2996
|
const wire = toWireDict(envelope);
|
|
2133
2997
|
await this._transport.send(wire);
|
|
@@ -2137,26 +3001,25 @@ var Agent = class {
|
|
|
2137
3001
|
/**
|
|
2138
3002
|
* Register with the relay server.
|
|
2139
3003
|
*
|
|
2140
|
-
* Calls POST /api/v1/register with agent name and public key.
|
|
2141
|
-
*
|
|
3004
|
+
* Calls POST /api/v1/register with agent name and public key. If the
|
|
3005
|
+
* address exists (409), repeats it with a proof that this agent holds the
|
|
3006
|
+
* key. Stores the returned API key on disk.
|
|
2142
3007
|
*/
|
|
2143
3008
|
async _registerWithRelay() {
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
headers: { "Content-Type": "application/json" },
|
|
2150
|
-
body: JSON.stringify({
|
|
2151
|
-
agent_name: this._config.name,
|
|
2152
|
-
public_key: publicKeyStr
|
|
2153
|
-
})
|
|
2154
|
-
}
|
|
2155
|
-
);
|
|
3009
|
+
const body = {
|
|
3010
|
+
agent_name: this._config.name,
|
|
3011
|
+
public_key: serializeVerifyKey(this._keyManager.verifyKey)
|
|
3012
|
+
};
|
|
3013
|
+
let resp = await this._postRegistration(body);
|
|
2156
3014
|
if (resp.status === 409) {
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
3015
|
+
const address3 = await this._addressOnRelay();
|
|
3016
|
+
resp = await this._postRegistration({
|
|
3017
|
+
...body,
|
|
3018
|
+
proof: this._registrationProof(address3)
|
|
3019
|
+
});
|
|
3020
|
+
if (resp.status === 409 || resp.status === 403) {
|
|
3021
|
+
throw this._ownershipNotProven(address3);
|
|
3022
|
+
}
|
|
2160
3023
|
}
|
|
2161
3024
|
if (!resp.ok) {
|
|
2162
3025
|
const text = await resp.text();
|
|
@@ -2165,22 +3028,140 @@ var Agent = class {
|
|
|
2165
3028
|
);
|
|
2166
3029
|
}
|
|
2167
3030
|
const data = await resp.json();
|
|
2168
|
-
|
|
3031
|
+
const address2 = data["address"];
|
|
3032
|
+
this._useAddress(address2);
|
|
2169
3033
|
this._token = data["token"];
|
|
2170
|
-
|
|
3034
|
+
const { name } = this._config;
|
|
3035
|
+
const token = this._token;
|
|
3036
|
+
this._tryToSave("token", () => this._keyManager.saveToken(name, token));
|
|
3037
|
+
this._saveAddress(address2);
|
|
3038
|
+
}
|
|
3039
|
+
/** This agent's address on the relay, for signing a proof. */
|
|
3040
|
+
async _addressOnRelay() {
|
|
3041
|
+
const { name, explicitRelayDomain, relayDomain, relayUrl } = this._config;
|
|
3042
|
+
if (explicitRelayDomain !== null) return `${name}::${explicitRelayDomain}`;
|
|
3043
|
+
const domain2 = await this._fetchRelayDomain();
|
|
3044
|
+
if (domain2 === null) return `${name}::${relayDomain}`;
|
|
3045
|
+
if (!this._relayMayUse(domain2)) {
|
|
3046
|
+
throw new UAMError(
|
|
3047
|
+
`The relay at ${relayUrl} says its domain is ${domain2}. Pass the domain option as ${JSON.stringify(domain2)} if that is right.`
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
return `${name}::${domain2}`;
|
|
3051
|
+
}
|
|
3052
|
+
/**
|
|
3053
|
+
* True if the relay's URL fits `domain`, so a proof can be signed for it.
|
|
3054
|
+
*
|
|
3055
|
+
* A proof names an address on one relay, so it is only signed for a domain
|
|
3056
|
+
* this URL belongs to. A relay on this machine may use any domain.
|
|
3057
|
+
*/
|
|
3058
|
+
_relayMayUse(domain2) {
|
|
3059
|
+
let host;
|
|
3060
|
+
try {
|
|
3061
|
+
host = new URL(this._config.relayUrl).hostname.toLowerCase();
|
|
3062
|
+
} catch {
|
|
3063
|
+
return false;
|
|
3064
|
+
}
|
|
3065
|
+
const wanted = domain2.toLowerCase();
|
|
3066
|
+
return host === wanted || host.endsWith(`.${wanted}`) || host === "localhost" || host === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
3067
|
+
}
|
|
3068
|
+
_postRegistration(body) {
|
|
3069
|
+
return fetch(`${this._config.relayUrl}/api/v1/register`, {
|
|
3070
|
+
method: "POST",
|
|
3071
|
+
headers: { "Content-Type": "application/json" },
|
|
3072
|
+
body: JSON.stringify(body)
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Sign a statement that this agent holds the key for `address`.
|
|
3077
|
+
*
|
|
3078
|
+
* The signed string must stay byte-identical to the Python SDK's.
|
|
3079
|
+
*/
|
|
3080
|
+
_registrationProof(address2) {
|
|
3081
|
+
const timestamp = utcTimestamp();
|
|
3082
|
+
const signature = signMessage(
|
|
3083
|
+
new TextEncoder().encode(`uam-register-v1
|
|
3084
|
+
${address2}
|
|
3085
|
+
${timestamp}`),
|
|
3086
|
+
this._keyManager.signingKey
|
|
3087
|
+
);
|
|
3088
|
+
return { timestamp, signature };
|
|
3089
|
+
}
|
|
3090
|
+
_ownershipNotProven(address2) {
|
|
3091
|
+
return new UAMError(
|
|
3092
|
+
`Address already registered, and this key could not prove ownership: ` + address2
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
/** Remember `address` for this relay, if the key folder allows it. */
|
|
3096
|
+
_saveAddress(address2) {
|
|
3097
|
+
const { name, relayUrl } = this._config;
|
|
3098
|
+
this._tryToSave(
|
|
3099
|
+
"address",
|
|
3100
|
+
() => this._keyManager.saveAddress(name, address2, relayUrl)
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
/** Saving is best effort: a key folder we can't write doesn't stop us. */
|
|
3104
|
+
_tryToSave(what, save) {
|
|
3105
|
+
try {
|
|
3106
|
+
save();
|
|
3107
|
+
} catch (err) {
|
|
3108
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3109
|
+
log.warn(`Could not save the ${what}: ${reason}`);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
/** Take `address` as ours; its domain is this relay's real domain. */
|
|
3113
|
+
_useAddress(address2) {
|
|
3114
|
+
this._address = address2;
|
|
3115
|
+
const domain2 = parseAddress(address2).domain;
|
|
3116
|
+
this._config.relayDomain = domain2;
|
|
3117
|
+
if (this._resolver instanceof SmartResolver) {
|
|
3118
|
+
this._resolver.relayDomain = domain2;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* The address the stored token belongs to.
|
|
3123
|
+
*
|
|
3124
|
+
* An explicit domain settles it. Otherwise the relay chooses the domain,
|
|
3125
|
+
* and it need not be the relay's hostname: use the address saved for this
|
|
3126
|
+
* relay, or ask the relay once and keep the answer.
|
|
3127
|
+
*/
|
|
3128
|
+
async _storedAddress() {
|
|
3129
|
+
const { name, explicitRelayDomain } = this._config;
|
|
3130
|
+
if (explicitRelayDomain !== null) return `${name}::${explicitRelayDomain}`;
|
|
3131
|
+
const saved = this._keyManager.loadAddress(name, this._config.relayUrl);
|
|
3132
|
+
if (saved) return saved;
|
|
3133
|
+
const domain2 = await this._fetchRelayDomain();
|
|
3134
|
+
if (domain2 === null) return `${name}::${this._config.relayDomain}`;
|
|
3135
|
+
const address2 = `${name}::${domain2}`;
|
|
3136
|
+
this._saveAddress(address2);
|
|
3137
|
+
return address2;
|
|
3138
|
+
}
|
|
3139
|
+
/** Ask the relay for its domain; null if it cannot say. */
|
|
3140
|
+
async _fetchRelayDomain() {
|
|
3141
|
+
const url = `${this._config.relayUrl}/.well-known/uam-relay.json`;
|
|
3142
|
+
try {
|
|
3143
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
3144
|
+
if (!resp.ok) return null;
|
|
3145
|
+
const data = await resp.json();
|
|
3146
|
+
const domain2 = data["relay_domain"];
|
|
3147
|
+
if (typeof domain2 !== "string" || domain2 === "") return null;
|
|
3148
|
+
return parseAddress(`${this._config.name}::${domain2}`).domain;
|
|
3149
|
+
} catch {
|
|
3150
|
+
return null;
|
|
3151
|
+
}
|
|
2171
3152
|
}
|
|
2172
3153
|
};
|
|
2173
3154
|
|
|
2174
3155
|
// src/cli/helpers.ts
|
|
2175
|
-
var
|
|
2176
|
-
var
|
|
3156
|
+
var import_node_fs5 = require("fs");
|
|
3157
|
+
var import_node_path6 = require("path");
|
|
2177
3158
|
var import_node_os4 = require("os");
|
|
2178
3159
|
function findAgentName(keyDir) {
|
|
2179
|
-
const dir = keyDir ?? (0,
|
|
2180
|
-
if (!(0,
|
|
3160
|
+
const dir = keyDir ?? (0, import_node_path6.join)(process.env["UAM_HOME"] ?? (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".uam"), "keys");
|
|
3161
|
+
if (!(0, import_node_fs5.existsSync)(dir)) {
|
|
2181
3162
|
return null;
|
|
2182
3163
|
}
|
|
2183
|
-
const keyFiles = (0,
|
|
3164
|
+
const keyFiles = (0, import_node_fs5.readdirSync)(dir).filter((f) => f.endsWith(".key")).sort();
|
|
2184
3165
|
if (keyFiles.length === 0) {
|
|
2185
3166
|
return null;
|
|
2186
3167
|
}
|
|
@@ -2200,21 +3181,21 @@ async function initCommand(options) {
|
|
|
2200
3181
|
try {
|
|
2201
3182
|
const cfg = new SDKConfig({ name: agentName, relayUrl: options.relay });
|
|
2202
3183
|
const km = new KeyManager(cfg.keyDir);
|
|
2203
|
-
const keyPath = (0,
|
|
2204
|
-
if ((0,
|
|
3184
|
+
const keyPath = (0, import_node_path7.join)(cfg.keyDir, `${agentName}.key`);
|
|
3185
|
+
if ((0, import_node_fs6.existsSync)(keyPath)) {
|
|
2205
3186
|
km.loadOrGenerate(agentName);
|
|
2206
|
-
const
|
|
3187
|
+
const address3 = km.loadAddress(agentName, cfg.relayUrl) ?? `${agentName}::${cfg.relayDomain}`;
|
|
2207
3188
|
const fp2 = publicKeyFingerprint(km.verifyKey);
|
|
2208
|
-
console.log(`Agent already initialized: ${
|
|
3189
|
+
console.log(`Agent already initialized: ${address3}`);
|
|
2209
3190
|
console.log(`Fingerprint: ${fp2}`);
|
|
2210
3191
|
return;
|
|
2211
3192
|
}
|
|
2212
3193
|
const agent = new Agent(agentName, { relay: options.relay });
|
|
2213
3194
|
await agent.connect();
|
|
2214
|
-
const
|
|
3195
|
+
const address2 = agent.address;
|
|
2215
3196
|
const fp = publicKeyFingerprint(agent._keyManager.verifyKey);
|
|
2216
3197
|
await agent.close();
|
|
2217
|
-
console.log(`Initialized agent: ${
|
|
3198
|
+
console.log(`Initialized agent: ${address2}`);
|
|
2218
3199
|
console.log(`Fingerprint: ${fp}`);
|
|
2219
3200
|
} catch (err) {
|
|
2220
3201
|
cliError(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2222,7 +3203,7 @@ async function initCommand(options) {
|
|
|
2222
3203
|
}
|
|
2223
3204
|
|
|
2224
3205
|
// src/cli/commands/send.ts
|
|
2225
|
-
async function sendCommand(
|
|
3206
|
+
async function sendCommand(address2, message, options) {
|
|
2226
3207
|
const agentName = options.name ?? findAgentName();
|
|
2227
3208
|
if (!agentName) {
|
|
2228
3209
|
cliError("No agent initialized. Run `uam init` first.");
|
|
@@ -2230,9 +3211,12 @@ async function sendCommand(address, message, options) {
|
|
|
2230
3211
|
try {
|
|
2231
3212
|
const agent = new Agent(agentName);
|
|
2232
3213
|
await agent.connect();
|
|
2233
|
-
const
|
|
3214
|
+
const result = await agent.send(address2, message);
|
|
2234
3215
|
await agent.close();
|
|
2235
|
-
console.log(`Message
|
|
3216
|
+
console.log(`Message to ${address2}: ${result.status} (id: ${result.messageId})`);
|
|
3217
|
+
if (result.status === "pending") {
|
|
3218
|
+
console.log(pendingNote(address2));
|
|
3219
|
+
}
|
|
2236
3220
|
} catch (err) {
|
|
2237
3221
|
cliError(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2238
3222
|
}
|
|
@@ -2284,24 +3268,24 @@ async function cardCommand(options) {
|
|
|
2284
3268
|
}
|
|
2285
3269
|
|
|
2286
3270
|
// src/cli/commands/whoami.ts
|
|
2287
|
-
var
|
|
2288
|
-
var
|
|
3271
|
+
var import_node_fs7 = require("fs");
|
|
3272
|
+
var import_node_path8 = require("path");
|
|
2289
3273
|
async function whoamiCommand(options) {
|
|
2290
3274
|
const agentName = options.name ?? findAgentName();
|
|
2291
3275
|
if (!agentName) {
|
|
2292
3276
|
cliError("No agent initialized. Run `uam init` first.");
|
|
2293
3277
|
}
|
|
2294
3278
|
const cfg = new SDKConfig({ name: agentName });
|
|
2295
|
-
const keyPath = (0,
|
|
2296
|
-
if (!(0,
|
|
3279
|
+
const keyPath = (0, import_node_path8.join)(cfg.keyDir, `${agentName}.key`);
|
|
3280
|
+
if (!(0, import_node_fs7.existsSync)(keyPath)) {
|
|
2297
3281
|
cliError("No agent initialized. Run `uam init` first.");
|
|
2298
3282
|
}
|
|
2299
3283
|
await sodiumReady;
|
|
2300
3284
|
const km = new KeyManager(cfg.keyDir);
|
|
2301
3285
|
km.loadOrGenerate(agentName);
|
|
2302
|
-
const
|
|
3286
|
+
const address2 = km.loadAddress(agentName, cfg.relayUrl) ?? `${agentName}::${cfg.relayDomain}`;
|
|
2303
3287
|
const fp = publicKeyFingerprint(km.verifyKey);
|
|
2304
|
-
console.log(`Address: ${
|
|
3288
|
+
console.log(`Address: ${address2}`);
|
|
2305
3289
|
console.log(`Fingerprint: ${fp}`);
|
|
2306
3290
|
console.log(`Key file: ${keyPath}`);
|
|
2307
3291
|
}
|
|
@@ -2372,7 +3356,7 @@ async function pendingCommand(options) {
|
|
|
2372
3356
|
}
|
|
2373
3357
|
|
|
2374
3358
|
// src/cli/commands/approve.ts
|
|
2375
|
-
async function approveCommand(
|
|
3359
|
+
async function approveCommand(address2, options) {
|
|
2376
3360
|
const agentName = options.name ?? findAgentName();
|
|
2377
3361
|
if (!agentName) {
|
|
2378
3362
|
cliError("No agent initialized. Run `uam init` first.");
|
|
@@ -2380,16 +3364,16 @@ async function approveCommand(address, options) {
|
|
|
2380
3364
|
try {
|
|
2381
3365
|
const agent = new Agent(agentName);
|
|
2382
3366
|
await agent.connect();
|
|
2383
|
-
await agent.approve(
|
|
3367
|
+
await agent.approve(address2);
|
|
2384
3368
|
await agent.close();
|
|
2385
|
-
console.log(`Approved: ${
|
|
3369
|
+
console.log(`Approved: ${address2}`);
|
|
2386
3370
|
} catch (err) {
|
|
2387
3371
|
cliError(`Error: ${err instanceof Error ? err.message : err}`);
|
|
2388
3372
|
}
|
|
2389
3373
|
}
|
|
2390
3374
|
|
|
2391
3375
|
// src/cli/commands/deny.ts
|
|
2392
|
-
async function denyCommand(
|
|
3376
|
+
async function denyCommand(address2, options) {
|
|
2393
3377
|
const agentName = options.name ?? findAgentName();
|
|
2394
3378
|
if (!agentName) {
|
|
2395
3379
|
cliError("No agent initialized. Run `uam init` first.");
|
|
@@ -2397,9 +3381,9 @@ async function denyCommand(address, options) {
|
|
|
2397
3381
|
try {
|
|
2398
3382
|
const agent = new Agent(agentName);
|
|
2399
3383
|
await agent.connect();
|
|
2400
|
-
await agent.deny(
|
|
3384
|
+
await agent.deny(address2);
|
|
2401
3385
|
await agent.close();
|
|
2402
|
-
console.log(`Denied: ${
|
|
3386
|
+
console.log(`Denied: ${address2}`);
|
|
2403
3387
|
} catch (err) {
|
|
2404
3388
|
cliError(`Error: ${err instanceof Error ? err.message : err}`);
|
|
2405
3389
|
}
|
|
@@ -2441,21 +3425,21 @@ async function unblockCommand(pattern, options) {
|
|
|
2441
3425
|
}
|
|
2442
3426
|
|
|
2443
3427
|
// src/cli/commands/contact.ts
|
|
2444
|
-
async function contactFingerprintCommand(
|
|
3428
|
+
async function contactFingerprintCommand(address2, options) {
|
|
2445
3429
|
const agentName = options.name ?? findAgentName();
|
|
2446
3430
|
const cfg = new SDKConfig({ name: agentName ?? "_probe" });
|
|
2447
3431
|
const book = new ContactBook(cfg.dataDir);
|
|
2448
3432
|
try {
|
|
2449
3433
|
book.open();
|
|
2450
|
-
const pkStr = book.getPublicKey(
|
|
3434
|
+
const pkStr = book.getPublicKey(address2);
|
|
2451
3435
|
book.close();
|
|
2452
3436
|
if (pkStr === null) {
|
|
2453
|
-
cliError(`Contact not found: ${
|
|
3437
|
+
cliError(`Contact not found: ${address2}`);
|
|
2454
3438
|
}
|
|
2455
3439
|
const vk = deserializeVerifyKey(pkStr);
|
|
2456
3440
|
const fp = publicKeyFingerprint(vk);
|
|
2457
3441
|
const shortFp = fp.slice(0, 16);
|
|
2458
|
-
console.log(`Address: ${
|
|
3442
|
+
console.log(`Address: ${address2}`);
|
|
2459
3443
|
console.log(`Fingerprint: ${shortFp}`);
|
|
2460
3444
|
console.log(`Full: ${fp}`);
|
|
2461
3445
|
} catch (err) {
|
|
@@ -2465,43 +3449,43 @@ async function contactFingerprintCommand(address, options) {
|
|
|
2465
3449
|
cliError(`Error: ${err instanceof Error ? err.message : err}`);
|
|
2466
3450
|
}
|
|
2467
3451
|
}
|
|
2468
|
-
async function contactVerifyCommand(
|
|
3452
|
+
async function contactVerifyCommand(address2, options) {
|
|
2469
3453
|
const agentName = options.name ?? findAgentName();
|
|
2470
3454
|
const cfg = new SDKConfig({ name: agentName ?? "_probe" });
|
|
2471
3455
|
const book = new ContactBook(cfg.dataDir);
|
|
2472
3456
|
try {
|
|
2473
3457
|
book.open();
|
|
2474
|
-
const pkStr = book.getPublicKey(
|
|
3458
|
+
const pkStr = book.getPublicKey(address2);
|
|
2475
3459
|
if (pkStr === null) {
|
|
2476
3460
|
book.close();
|
|
2477
|
-
cliError(`Contact not found: ${
|
|
3461
|
+
cliError(`Contact not found: ${address2}`);
|
|
2478
3462
|
}
|
|
2479
3463
|
const vk = deserializeVerifyKey(pkStr);
|
|
2480
3464
|
const fp = publicKeyFingerprint(vk);
|
|
2481
|
-
console.log(`Fingerprint for ${
|
|
2482
|
-
book.addContact(
|
|
3465
|
+
console.log(`Fingerprint for ${address2}: ${fp.slice(0, 16)}`);
|
|
3466
|
+
book.addContact(address2, pkStr, {
|
|
2483
3467
|
trustState: "verified",
|
|
2484
3468
|
trustSource: "manual-verify"
|
|
2485
3469
|
});
|
|
2486
3470
|
book.close();
|
|
2487
|
-
console.log(`Contact ${
|
|
3471
|
+
console.log(`Contact ${address2} verified. Trust state: verified`);
|
|
2488
3472
|
} catch (err) {
|
|
2489
3473
|
cliError(`Error: ${err instanceof Error ? err.message : err}`);
|
|
2490
3474
|
}
|
|
2491
3475
|
}
|
|
2492
|
-
async function contactRemoveCommand(
|
|
3476
|
+
async function contactRemoveCommand(address2, options) {
|
|
2493
3477
|
const agentName = options.name ?? findAgentName();
|
|
2494
3478
|
const cfg = new SDKConfig({ name: agentName ?? "_probe" });
|
|
2495
3479
|
const book = new ContactBook(cfg.dataDir);
|
|
2496
3480
|
try {
|
|
2497
3481
|
book.open();
|
|
2498
|
-
const removed = book.removeContact(
|
|
3482
|
+
const removed = book.removeContact(address2);
|
|
2499
3483
|
book.close();
|
|
2500
3484
|
if (!removed) {
|
|
2501
|
-
cliError(`Contact not found: ${
|
|
3485
|
+
cliError(`Contact not found: ${address2}`);
|
|
2502
3486
|
}
|
|
2503
3487
|
console.log(
|
|
2504
|
-
`Contact ${
|
|
3488
|
+
`Contact ${address2} removed. Future messages will re-resolve the public key.`
|
|
2505
3489
|
);
|
|
2506
3490
|
} catch (err) {
|
|
2507
3491
|
cliError(`Error: ${err instanceof Error ? err.message : err}`);
|
|
@@ -2510,7 +3494,7 @@ async function contactRemoveCommand(address, options) {
|
|
|
2510
3494
|
|
|
2511
3495
|
// src/cli/commands/verify-domain.ts
|
|
2512
3496
|
init_dns_verifier();
|
|
2513
|
-
async function verifyDomainCommand(
|
|
3497
|
+
async function verifyDomainCommand(domain2, options) {
|
|
2514
3498
|
const agentName = options.name ?? findAgentName();
|
|
2515
3499
|
if (!agentName) {
|
|
2516
3500
|
cliError("No agent initialized. Run `uam init` first.");
|
|
@@ -2523,27 +3507,27 @@ async function verifyDomainCommand(domain, options) {
|
|
|
2523
3507
|
const pubkey = agent.publicKey;
|
|
2524
3508
|
const relayUrl = agent._config.relayUrl;
|
|
2525
3509
|
const txtValue = generateTxtRecord(pubkey, relayUrl);
|
|
2526
|
-
console.log(`Add this DNS TXT record to verify ${
|
|
3510
|
+
console.log(`Add this DNS TXT record to verify ${domain2}:`);
|
|
2527
3511
|
console.log();
|
|
2528
|
-
console.log(` Host: _uam.${
|
|
3512
|
+
console.log(` Host: _uam.${domain2}`);
|
|
2529
3513
|
console.log(` Type: TXT`);
|
|
2530
3514
|
console.log(` Value: ${txtValue}`);
|
|
2531
3515
|
console.log();
|
|
2532
3516
|
console.log(`Or serve this HTTPS fallback:`);
|
|
2533
3517
|
console.log();
|
|
2534
|
-
console.log(` URL: https://${
|
|
3518
|
+
console.log(` URL: https://${domain2}/.well-known/uam.json`);
|
|
2535
3519
|
console.log();
|
|
2536
3520
|
console.log(`See documentation for .well-known/uam.json format.`);
|
|
2537
3521
|
console.log();
|
|
2538
3522
|
console.log("Polling for verification...");
|
|
2539
|
-
const verified = await agent.verifyDomain(
|
|
3523
|
+
const verified = await agent.verifyDomain(domain2, {
|
|
2540
3524
|
timeout: timeout * 1e3,
|
|
2541
3525
|
pollInterval: pollInterval * 1e3
|
|
2542
3526
|
});
|
|
2543
3527
|
await agent.close();
|
|
2544
3528
|
if (verified) {
|
|
2545
3529
|
console.log(
|
|
2546
|
-
`
|
|
3530
|
+
`Domain checked by ${agent.address.split("::")[1] ?? "your relay"}: ${agent.address} is now Tier 2 via ${domain2}.`
|
|
2547
3531
|
);
|
|
2548
3532
|
} else {
|
|
2549
3533
|
console.log(
|
|
@@ -2557,7 +3541,7 @@ async function verifyDomainCommand(domain, options) {
|
|
|
2557
3541
|
|
|
2558
3542
|
// src/cli/index.ts
|
|
2559
3543
|
var program = new import_commander.Command();
|
|
2560
|
-
program.name("uam").description("Universal Agent Messaging CLI").version(
|
|
3544
|
+
program.name("uam").description("Universal Agent Messaging CLI").version(SDK_VERSION);
|
|
2561
3545
|
program.option("-n, --name <name>", "Agent name (auto-detected from ~/.uam/keys/)");
|
|
2562
3546
|
program.command("init").description("Initialize a new agent: generate keys and register with relay").option("-n, --name <name>", "Agent name").option("-r, --relay <url>", "Relay URL (default: relay.youam.network)").action(async (opts) => {
|
|
2563
3547
|
const globalName = program.opts().name;
|
|
@@ -2566,9 +3550,9 @@ program.command("init").description("Initialize a new agent: generate keys and r
|
|
|
2566
3550
|
relay: opts.relay
|
|
2567
3551
|
});
|
|
2568
3552
|
});
|
|
2569
|
-
program.command("send <address> <message>").description("Send a message to another agent").action(async (
|
|
3553
|
+
program.command("send <address> <message>").description("Send a message to another agent").action(async (address2, message) => {
|
|
2570
3554
|
const globalName = program.opts().name;
|
|
2571
|
-
await sendCommand(
|
|
3555
|
+
await sendCommand(address2, message, { name: globalName });
|
|
2572
3556
|
});
|
|
2573
3557
|
program.command("inbox").description("Check your inbox for pending messages").option("-l, --limit <number>", "Max messages to retrieve", "20").action(async (opts) => {
|
|
2574
3558
|
const globalName = program.opts().name;
|
|
@@ -2590,21 +3574,21 @@ program.command("contacts").description("List known contacts from the local cont
|
|
|
2590
3574
|
await contactsCommand({ name: globalName });
|
|
2591
3575
|
});
|
|
2592
3576
|
var contactCmd = program.command("contact").description("Contact management commands (fingerprint, verify, remove)");
|
|
2593
|
-
contactCmd.command("fingerprint <address>").description("Display the fingerprint for a known contact's public key").action(async (
|
|
3577
|
+
contactCmd.command("fingerprint <address>").description("Display the fingerprint for a known contact's public key").action(async (address2) => {
|
|
2594
3578
|
const globalName = program.opts().name;
|
|
2595
|
-
await contactFingerprintCommand(
|
|
3579
|
+
await contactFingerprintCommand(address2, { name: globalName });
|
|
2596
3580
|
});
|
|
2597
|
-
contactCmd.command("verify <address>").description("Manually verify a contact, upgrading trust state to verified").option("-y, --yes", "Skip confirmation prompt").action(async (
|
|
3581
|
+
contactCmd.command("verify <address>").description("Manually verify a contact, upgrading trust state to verified").option("-y, --yes", "Skip confirmation prompt").action(async (address2, opts) => {
|
|
2598
3582
|
const globalName = program.opts().name;
|
|
2599
|
-
await contactVerifyCommand(
|
|
3583
|
+
await contactVerifyCommand(address2, { name: globalName, yes: opts.yes });
|
|
2600
3584
|
});
|
|
2601
|
-
contactCmd.command("remove <address>").description("Remove a contact from the contact book").action(async (
|
|
3585
|
+
contactCmd.command("remove <address>").description("Remove a contact from the contact book").action(async (address2) => {
|
|
2602
3586
|
const globalName = program.opts().name;
|
|
2603
|
-
await contactRemoveCommand(
|
|
3587
|
+
await contactRemoveCommand(address2, { name: globalName });
|
|
2604
3588
|
});
|
|
2605
|
-
program.command("verify-domain <domain>").description("Verify domain ownership for Tier 2 DNS-verified status").option("-t, --timeout <seconds>", "Polling timeout in seconds", "300").option("--poll-interval <seconds>", "Polling interval in seconds", "10").action(async (
|
|
3589
|
+
program.command("verify-domain <domain>").description("Verify domain ownership for Tier 2 DNS-verified status").option("-t, --timeout <seconds>", "Polling timeout in seconds", "300").option("--poll-interval <seconds>", "Polling interval in seconds", "10").action(async (domain2, opts) => {
|
|
2606
3590
|
const globalName = program.opts().name;
|
|
2607
|
-
await verifyDomainCommand(
|
|
3591
|
+
await verifyDomainCommand(domain2, {
|
|
2608
3592
|
name: globalName,
|
|
2609
3593
|
timeout: parseInt(opts.timeout, 10),
|
|
2610
3594
|
pollInterval: parseInt(opts.pollInterval, 10)
|
|
@@ -2614,13 +3598,13 @@ program.command("pending").description("List pending handshake requests awaiting
|
|
|
2614
3598
|
const globalName = program.opts().name;
|
|
2615
3599
|
await pendingCommand({ name: globalName });
|
|
2616
3600
|
});
|
|
2617
|
-
program.command("approve <address>").description("Approve a pending handshake request").action(async (
|
|
3601
|
+
program.command("approve <address>").description("Approve a pending handshake request").action(async (address2) => {
|
|
2618
3602
|
const globalName = program.opts().name;
|
|
2619
|
-
await approveCommand(
|
|
3603
|
+
await approveCommand(address2, { name: globalName });
|
|
2620
3604
|
});
|
|
2621
|
-
program.command("deny <address>").description("Deny a pending handshake request").action(async (
|
|
3605
|
+
program.command("deny <address>").description("Deny a pending handshake request").action(async (address2) => {
|
|
2622
3606
|
const globalName = program.opts().name;
|
|
2623
|
-
await denyCommand(
|
|
3607
|
+
await denyCommand(address2, { name: globalName });
|
|
2624
3608
|
});
|
|
2625
3609
|
program.command("block <pattern>").description("Block an address or domain pattern (e.g., spammer::evil.com or *::evil.com)").action(async (pattern) => {
|
|
2626
3610
|
const globalName = program.opts().name;
|