@youam/sdk 0.2.1 → 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/README.md +7 -2
- package/dist/cli/index.cjs +448 -176
- package/dist/index.cjs +1053 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +184 -7
- package/dist/index.d.ts +184 -7
- package/dist/index.js +1047 -129
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -39,11 +39,11 @@ function extractPublicKey(tags) {
|
|
|
39
39
|
}
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
-
async function queryUamTxt(
|
|
42
|
+
async function queryUamTxt(domain2, timeout) {
|
|
43
43
|
const controller = new AbortController();
|
|
44
44
|
const timer = timeout != null ? setTimeout(() => controller.abort(), timeout) : void 0;
|
|
45
45
|
try {
|
|
46
|
-
const records = await dnsResolveTxt(`_uam.${
|
|
46
|
+
const records = await dnsResolveTxt(`_uam.${domain2}`);
|
|
47
47
|
const results = [];
|
|
48
48
|
for (const parts of records) {
|
|
49
49
|
const txtValue = parts.join("");
|
|
@@ -58,8 +58,8 @@ async function queryUamTxt(domain, timeout) {
|
|
|
58
58
|
if (timer != null) clearTimeout(timer);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
async function resolveKeyViaHttps(agentName,
|
|
62
|
-
const url = `https://${
|
|
61
|
+
async function resolveKeyViaHttps(agentName, domain2, timeout) {
|
|
62
|
+
const url = `https://${domain2}/.well-known/uam.json`;
|
|
63
63
|
try {
|
|
64
64
|
const resp = await fetch(url, {
|
|
65
65
|
signal: AbortSignal.timeout(timeout ?? 1e4)
|
|
@@ -99,32 +99,43 @@ __export(protocol_exports, {
|
|
|
99
99
|
EnvelopeTooLargeError: () => EnvelopeTooLargeError,
|
|
100
100
|
InvalidAddressError: () => InvalidAddressError,
|
|
101
101
|
InvalidContactCardError: () => InvalidContactCardError,
|
|
102
|
+
InvalidDidKeyError: () => InvalidDidKeyError,
|
|
102
103
|
InvalidEnvelopeError: () => InvalidEnvelopeError,
|
|
103
104
|
KeyPinningError: () => KeyPinningError,
|
|
104
105
|
MAX_ENVELOPE_AGE: () => MAX_ENVELOPE_AGE,
|
|
105
106
|
MAX_ENVELOPE_SIZE: () => MAX_ENVELOPE_SIZE,
|
|
106
107
|
MessageType: () => MessageType,
|
|
108
|
+
SendRefusedError: () => SendRefusedError,
|
|
107
109
|
SignatureError: () => SignatureError,
|
|
108
110
|
SignatureVerificationError: () => SignatureVerificationError,
|
|
111
|
+
SignedObjectError: () => SignedObjectError,
|
|
109
112
|
UAMError: () => UAMError,
|
|
110
113
|
UAM_VERSION: () => UAM_VERSION,
|
|
114
|
+
WebAuthnError: () => WebAuthnError,
|
|
115
|
+
approvalChallenge: () => approvalChallenge,
|
|
111
116
|
b64Decode: () => b64Decode,
|
|
112
117
|
b64Encode: () => b64Encode,
|
|
118
|
+
canonical: () => canonical,
|
|
113
119
|
canonicalize: () => canonicalize,
|
|
114
120
|
contactCardFromDict: () => contactCardFromDict,
|
|
115
121
|
contactCardToDict: () => contactCardToDict,
|
|
116
122
|
createContactCard: () => createContactCard,
|
|
117
123
|
createEnvelope: () => createEnvelope,
|
|
124
|
+
credentialDid: () => credentialDid,
|
|
125
|
+
decodeDidKey: () => decodeDidKey,
|
|
118
126
|
decryptPayload: () => decryptPayload,
|
|
119
127
|
decryptPayloadAnonymous: () => decryptPayloadAnonymous,
|
|
120
128
|
deserializeSigningKey: () => deserializeSigningKey,
|
|
121
129
|
deserializeVerifyKey: () => deserializeVerifyKey,
|
|
130
|
+
encodeDidKey: () => encodeDidKey,
|
|
122
131
|
encryptPayload: () => encryptPayload,
|
|
123
132
|
encryptPayloadAnonymous: () => encryptPayloadAnonymous,
|
|
124
133
|
fromWireDict: () => fromWireDict,
|
|
125
134
|
generateKeypair: () => generateKeypair,
|
|
126
135
|
generateNonce: () => generateNonce,
|
|
136
|
+
objectHash: () => objectHash,
|
|
127
137
|
parseAddress: () => parseAddress,
|
|
138
|
+
parseBody: () => parseBody,
|
|
128
139
|
publicKeyFingerprint: () => publicKeyFingerprint,
|
|
129
140
|
serializeSigningKey: () => serializeSigningKey,
|
|
130
141
|
serializeVerifyKey: () => serializeVerifyKey,
|
|
@@ -133,8 +144,11 @@ __export(protocol_exports, {
|
|
|
133
144
|
toWireDict: () => toWireDict,
|
|
134
145
|
utcTimestamp: () => utcTimestamp,
|
|
135
146
|
validateEnvelopeSize: () => validateEnvelopeSize,
|
|
147
|
+
validateSignedObject: () => validate,
|
|
148
|
+
verifyAssertion: () => verifyAssertion,
|
|
136
149
|
verifyContactCard: () => verifyContactCard,
|
|
137
150
|
verifyEnvelope: () => verifyEnvelope,
|
|
151
|
+
verifyRegistration: () => verifyRegistration,
|
|
138
152
|
verifySignature: () => verifySignature
|
|
139
153
|
});
|
|
140
154
|
|
|
@@ -229,6 +243,18 @@ var KeyPinningError = class _KeyPinningError extends UAMError {
|
|
|
229
243
|
Object.setPrototypeOf(this, _KeyPinningError.prototype);
|
|
230
244
|
}
|
|
231
245
|
};
|
|
246
|
+
var SendRefusedError = class extends UAMError {
|
|
247
|
+
messageId;
|
|
248
|
+
code;
|
|
249
|
+
detail;
|
|
250
|
+
constructor(messageId, code, detail) {
|
|
251
|
+
super(`Relay refused message ${messageId}: [${code}] ${detail ?? ""}`.trimEnd());
|
|
252
|
+
this.name = "SendRefusedError";
|
|
253
|
+
this.messageId = messageId;
|
|
254
|
+
this.code = code;
|
|
255
|
+
this.detail = detail;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
232
258
|
|
|
233
259
|
// src/protocol/address.ts
|
|
234
260
|
var ADDRESS_RE = /^(?<agent>[a-z0-9][a-z0-9_-]{0,62}[a-z0-9]|[a-z0-9])::(?<domain>[a-z0-9](?:[a-z0-9.-]{0,253}[a-z0-9])?)$/;
|
|
@@ -355,11 +381,11 @@ function encryptPayload(plaintext, senderSigningKey, recipientVerifyKey) {
|
|
|
355
381
|
try {
|
|
356
382
|
const senderCurve = sodium.crypto_sign_ed25519_sk_to_curve25519(senderSigningKey);
|
|
357
383
|
const recipientCurve = sodium.crypto_sign_ed25519_pk_to_curve25519(recipientVerifyKey);
|
|
358
|
-
const
|
|
359
|
-
const encrypted = sodium.crypto_box_easy(plaintext,
|
|
360
|
-
const combined = new Uint8Array(
|
|
361
|
-
combined.set(
|
|
362
|
-
combined.set(encrypted,
|
|
384
|
+
const nonce2 = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
|
|
385
|
+
const encrypted = sodium.crypto_box_easy(plaintext, nonce2, recipientCurve, senderCurve);
|
|
386
|
+
const combined = new Uint8Array(nonce2.length + encrypted.length);
|
|
387
|
+
combined.set(nonce2, 0);
|
|
388
|
+
combined.set(encrypted, nonce2.length);
|
|
363
389
|
return b64Encode(combined);
|
|
364
390
|
} catch (err) {
|
|
365
391
|
if (err instanceof EncryptionError) throw err;
|
|
@@ -369,11 +395,11 @@ function encryptPayload(plaintext, senderSigningKey, recipientVerifyKey) {
|
|
|
369
395
|
function decryptPayload(ciphertextB64, recipientSigningKey, senderVerifyKey) {
|
|
370
396
|
try {
|
|
371
397
|
const combined = b64Decode(ciphertextB64);
|
|
372
|
-
const
|
|
398
|
+
const nonce2 = combined.slice(0, sodium.crypto_box_NONCEBYTES);
|
|
373
399
|
const ciphertext = combined.slice(sodium.crypto_box_NONCEBYTES);
|
|
374
400
|
const recipientCurve = sodium.crypto_sign_ed25519_sk_to_curve25519(recipientSigningKey);
|
|
375
401
|
const senderCurve = sodium.crypto_sign_ed25519_pk_to_curve25519(senderVerifyKey);
|
|
376
|
-
return sodium.crypto_box_open_easy(ciphertext,
|
|
402
|
+
return sodium.crypto_box_open_easy(ciphertext, nonce2, senderCurve, recipientCurve);
|
|
377
403
|
} catch (err) {
|
|
378
404
|
if (err instanceof DecryptionError) throw err;
|
|
379
405
|
throw new DecryptionError(err instanceof Error ? err.message : String(err));
|
|
@@ -481,7 +507,7 @@ async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaint
|
|
|
481
507
|
parseAddress(fromAddress);
|
|
482
508
|
parseAddress(toAddress);
|
|
483
509
|
const messageId = randomUUID();
|
|
484
|
-
const
|
|
510
|
+
const nonce2 = generateNonce();
|
|
485
511
|
const timestamp = utcTimestamp();
|
|
486
512
|
let encryptedPayload;
|
|
487
513
|
if (typeValue === "handshake.request" /* HANDSHAKE_REQUEST */) {
|
|
@@ -500,7 +526,7 @@ async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaint
|
|
|
500
526
|
toAddress,
|
|
501
527
|
timestamp,
|
|
502
528
|
type: typeValue,
|
|
503
|
-
nonce,
|
|
529
|
+
nonce: nonce2,
|
|
504
530
|
payload: encryptedPayload,
|
|
505
531
|
signature: "",
|
|
506
532
|
threadId: options?.threadId,
|
|
@@ -519,7 +545,7 @@ async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaint
|
|
|
519
545
|
toAddress,
|
|
520
546
|
timestamp,
|
|
521
547
|
type: typeValue,
|
|
522
|
-
nonce,
|
|
548
|
+
nonce: nonce2,
|
|
523
549
|
payload: encryptedPayload,
|
|
524
550
|
signature,
|
|
525
551
|
threadId: options?.threadId,
|
|
@@ -611,8 +637,8 @@ function contactCardFromDict(d, options) {
|
|
|
611
637
|
}
|
|
612
638
|
return card;
|
|
613
639
|
}
|
|
614
|
-
async function createContactCard(
|
|
615
|
-
parseAddress(
|
|
640
|
+
async function createContactCard(address2, displayName, relay, signingKey, options) {
|
|
641
|
+
parseAddress(address2);
|
|
616
642
|
const verifyKey = signingKey.slice(32);
|
|
617
643
|
const pubKeyB64 = serializeVerifyKey(verifyKey);
|
|
618
644
|
const fp = publicKeyFingerprint(verifyKey);
|
|
@@ -622,7 +648,7 @@ async function createContactCard(address, displayName, relay, signingKey, option
|
|
|
622
648
|
];
|
|
623
649
|
const tempCard = {
|
|
624
650
|
version: UAM_VERSION,
|
|
625
|
-
address,
|
|
651
|
+
address: address2,
|
|
626
652
|
displayName,
|
|
627
653
|
description: options?.description,
|
|
628
654
|
system: options?.system,
|
|
@@ -639,7 +665,7 @@ async function createContactCard(address, displayName, relay, signingKey, option
|
|
|
639
665
|
const signature = signMessage(canonicalize(signable), signingKey);
|
|
640
666
|
return Object.freeze({
|
|
641
667
|
version: UAM_VERSION,
|
|
642
|
-
address,
|
|
668
|
+
address: address2,
|
|
643
669
|
displayName,
|
|
644
670
|
description: options?.description,
|
|
645
671
|
system: options?.system,
|
|
@@ -666,8 +692,778 @@ function verifyContactCard(card) {
|
|
|
666
692
|
verifySignature(canonicalize(signable), card.signature, vk);
|
|
667
693
|
}
|
|
668
694
|
|
|
695
|
+
// src/protocol/signed.ts
|
|
696
|
+
import { createHash as createHash2 } from "crypto";
|
|
697
|
+
|
|
698
|
+
// src/protocol/didkey.ts
|
|
699
|
+
var B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
700
|
+
var PREFIX = {
|
|
701
|
+
ed25519: new Uint8Array([237, 1]),
|
|
702
|
+
p256: new Uint8Array([128, 36])
|
|
703
|
+
};
|
|
704
|
+
var LENGTH = { ed25519: 32, p256: 33 };
|
|
705
|
+
var InvalidDidKeyError = class extends UAMError {
|
|
706
|
+
constructor(message) {
|
|
707
|
+
super(message);
|
|
708
|
+
this.name = "InvalidDidKeyError";
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
function b58encode(data) {
|
|
712
|
+
let n = 0n;
|
|
713
|
+
for (const byte of data) n = n << 8n | BigInt(byte);
|
|
714
|
+
let out = "";
|
|
715
|
+
while (n > 0n) {
|
|
716
|
+
out = B58[Number(n % 58n)] + out;
|
|
717
|
+
n /= 58n;
|
|
718
|
+
}
|
|
719
|
+
let zeros = 0;
|
|
720
|
+
while (zeros < data.length && data[zeros] === 0) zeros += 1;
|
|
721
|
+
return "1".repeat(zeros) + out;
|
|
722
|
+
}
|
|
723
|
+
function b58decode(text) {
|
|
724
|
+
let n = 0n;
|
|
725
|
+
for (const c of text) {
|
|
726
|
+
const i = B58.indexOf(c);
|
|
727
|
+
if (i < 0) throw new InvalidDidKeyError(`not base58btc: ${JSON.stringify(c)}`);
|
|
728
|
+
n = n * 58n + BigInt(i);
|
|
729
|
+
}
|
|
730
|
+
const body = [];
|
|
731
|
+
while (n > 0n) {
|
|
732
|
+
body.unshift(Number(n & 0xffn));
|
|
733
|
+
n >>= 8n;
|
|
734
|
+
}
|
|
735
|
+
let zeros = 0;
|
|
736
|
+
while (zeros < text.length && text[zeros] === "1") zeros += 1;
|
|
737
|
+
return new Uint8Array([...new Array(zeros).fill(0), ...body]);
|
|
738
|
+
}
|
|
739
|
+
function checkKey(kind, key) {
|
|
740
|
+
if (key.length !== LENGTH[kind]) {
|
|
741
|
+
throw new InvalidDidKeyError(`${kind} key must be ${LENGTH[kind]} bytes`);
|
|
742
|
+
}
|
|
743
|
+
if (kind === "p256" && key[0] !== 2 && key[0] !== 3) {
|
|
744
|
+
throw new InvalidDidKeyError("P-256 key must be a compressed point");
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function encodeDidKey(kind, publicKey) {
|
|
748
|
+
if (!(kind in PREFIX)) throw new InvalidDidKeyError(`unsupported key type: ${kind}`);
|
|
749
|
+
checkKey(kind, publicKey);
|
|
750
|
+
return "did:key:z" + b58encode(new Uint8Array([...PREFIX[kind], ...publicKey]));
|
|
751
|
+
}
|
|
752
|
+
function decodeDidKey(did2) {
|
|
753
|
+
if (typeof did2 !== "string" || !did2.startsWith("did:key:z")) {
|
|
754
|
+
throw new InvalidDidKeyError("not a did:key with base58btc multibase");
|
|
755
|
+
}
|
|
756
|
+
const raw = b58decode(did2.slice("did:key:z".length));
|
|
757
|
+
for (const kind of Object.keys(PREFIX)) {
|
|
758
|
+
const prefix = PREFIX[kind];
|
|
759
|
+
if (raw.length >= prefix.length && prefix.every((b, i) => raw[i] === b)) {
|
|
760
|
+
const key = raw.slice(prefix.length);
|
|
761
|
+
checkKey(kind, key);
|
|
762
|
+
return { kind, key };
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
throw new InvalidDidKeyError("unsupported key type");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/protocol/signed.ts
|
|
769
|
+
var MAX_INT = 2 ** 53 - 1;
|
|
770
|
+
var GRANT_MAX_DAYS = 90;
|
|
771
|
+
var INVITE_MAX_DAYS = 7;
|
|
772
|
+
var SignedObjectError = class extends UAMError {
|
|
773
|
+
constructor(message) {
|
|
774
|
+
super(message);
|
|
775
|
+
this.name = "SignedObjectError";
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
var has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
779
|
+
var Parser = class {
|
|
780
|
+
constructor(s) {
|
|
781
|
+
this.s = s;
|
|
782
|
+
}
|
|
783
|
+
i = 0;
|
|
784
|
+
fail(why) {
|
|
785
|
+
throw new SignedObjectError(`not JSON or outside the profile at ${this.i}: ${why}`);
|
|
786
|
+
}
|
|
787
|
+
ws() {
|
|
788
|
+
while (this.i < this.s.length && " \n\r".includes(this.s[this.i])) this.i += 1;
|
|
789
|
+
}
|
|
790
|
+
document() {
|
|
791
|
+
this.ws();
|
|
792
|
+
if (this.s[this.i] !== "{") this.fail("a body must be a JSON object");
|
|
793
|
+
const value = this.value();
|
|
794
|
+
this.ws();
|
|
795
|
+
if (this.i !== this.s.length) this.fail("trailing characters");
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
value() {
|
|
799
|
+
this.ws();
|
|
800
|
+
const c = this.s[this.i];
|
|
801
|
+
if (c === "{") return this.object();
|
|
802
|
+
if (c === "[") return this.array();
|
|
803
|
+
if (c === '"') return this.string();
|
|
804
|
+
if (c === "-" || c !== void 0 && c >= "0" && c <= "9") return this.number();
|
|
805
|
+
for (const [word, v] of [["true", true], ["false", false]]) {
|
|
806
|
+
if (this.s.startsWith(word, this.i)) {
|
|
807
|
+
this.i += word.length;
|
|
808
|
+
return v;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (this.s.startsWith("null", this.i)) this.fail("null is not allowed");
|
|
812
|
+
return this.fail("unexpected character");
|
|
813
|
+
}
|
|
814
|
+
object() {
|
|
815
|
+
this.i += 1;
|
|
816
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
817
|
+
this.ws();
|
|
818
|
+
if (this.s[this.i] === "}") {
|
|
819
|
+
this.i += 1;
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
for (; ; ) {
|
|
823
|
+
this.ws();
|
|
824
|
+
if (this.s[this.i] !== '"') this.fail("expected a key");
|
|
825
|
+
const key = this.string();
|
|
826
|
+
if (has(out, key)) this.fail(`duplicate key: ${JSON.stringify(key)}`);
|
|
827
|
+
this.ws();
|
|
828
|
+
if (this.s[this.i] !== ":") this.fail("expected ':'");
|
|
829
|
+
this.i += 1;
|
|
830
|
+
Object.defineProperty(out, key, {
|
|
831
|
+
value: this.value(),
|
|
832
|
+
enumerable: true,
|
|
833
|
+
writable: true,
|
|
834
|
+
configurable: true
|
|
835
|
+
});
|
|
836
|
+
this.ws();
|
|
837
|
+
if (this.s[this.i] === ",") {
|
|
838
|
+
this.i += 1;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (this.s[this.i] === "}") {
|
|
842
|
+
this.i += 1;
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
this.fail("expected ',' or '}'");
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
array() {
|
|
849
|
+
this.i += 1;
|
|
850
|
+
const out = [];
|
|
851
|
+
this.ws();
|
|
852
|
+
if (this.s[this.i] === "]") {
|
|
853
|
+
this.i += 1;
|
|
854
|
+
return out;
|
|
855
|
+
}
|
|
856
|
+
for (; ; ) {
|
|
857
|
+
out.push(this.value());
|
|
858
|
+
this.ws();
|
|
859
|
+
if (this.s[this.i] === ",") {
|
|
860
|
+
this.i += 1;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (this.s[this.i] === "]") {
|
|
864
|
+
this.i += 1;
|
|
865
|
+
return out;
|
|
866
|
+
}
|
|
867
|
+
this.fail("expected ',' or ']'");
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
string() {
|
|
871
|
+
this.i += 1;
|
|
872
|
+
let out = "";
|
|
873
|
+
for (; ; ) {
|
|
874
|
+
const c = this.s[this.i];
|
|
875
|
+
if (c === void 0) this.fail("unterminated string");
|
|
876
|
+
this.i += 1;
|
|
877
|
+
if (c === '"') break;
|
|
878
|
+
if (c === "\\") {
|
|
879
|
+
const e = this.s[this.i];
|
|
880
|
+
this.i += 1;
|
|
881
|
+
const simple = {
|
|
882
|
+
'"': '"',
|
|
883
|
+
"\\": "\\",
|
|
884
|
+
"/": "/",
|
|
885
|
+
b: "\b",
|
|
886
|
+
f: "\f",
|
|
887
|
+
n: "\n",
|
|
888
|
+
r: "\r",
|
|
889
|
+
t: " "
|
|
890
|
+
};
|
|
891
|
+
if (e !== void 0 && has(simple, e)) {
|
|
892
|
+
out += simple[e];
|
|
893
|
+
} else if (e === "u") {
|
|
894
|
+
const hex = this.s.slice(this.i, this.i + 4);
|
|
895
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) this.fail("bad \\u escape");
|
|
896
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
897
|
+
this.i += 4;
|
|
898
|
+
} else {
|
|
899
|
+
this.fail("bad escape");
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
if (c < " ") this.fail("control character in string");
|
|
903
|
+
out += c;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
checkString(out);
|
|
907
|
+
return out;
|
|
908
|
+
}
|
|
909
|
+
number() {
|
|
910
|
+
const m = /^-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/.exec(this.s.slice(this.i));
|
|
911
|
+
if (!m) return this.fail("bad number");
|
|
912
|
+
const text = m[0];
|
|
913
|
+
if (m[2] !== void 0 || m[3] !== void 0) this.fail(`floats are not allowed: ${text}`);
|
|
914
|
+
if (text === "-0") this.fail("-0 is not allowed");
|
|
915
|
+
const big = BigInt(text);
|
|
916
|
+
if (big > BigInt(MAX_INT) || big < -BigInt(MAX_INT)) this.fail(`integer out of range: ${text}`);
|
|
917
|
+
this.i += text.length;
|
|
918
|
+
return Number(big);
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
function parseBody(text) {
|
|
922
|
+
let s;
|
|
923
|
+
if (typeof text === "string") {
|
|
924
|
+
s = text;
|
|
925
|
+
} else {
|
|
926
|
+
try {
|
|
927
|
+
s = new TextDecoder("utf-8", { fatal: true }).decode(text);
|
|
928
|
+
} catch {
|
|
929
|
+
throw new SignedObjectError("body is not UTF-8");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return new Parser(s).document();
|
|
933
|
+
}
|
|
934
|
+
function checkString(value) {
|
|
935
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
936
|
+
const code = value.charCodeAt(i);
|
|
937
|
+
if (code < 32 || code > 126) {
|
|
938
|
+
throw new SignedObjectError("strings must be printable ASCII");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function isPlainObject(value) {
|
|
943
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
944
|
+
const proto = Object.getPrototypeOf(value);
|
|
945
|
+
return proto === null || proto === Object.prototype;
|
|
946
|
+
}
|
|
947
|
+
function encode(value) {
|
|
948
|
+
if (value === true) return "true";
|
|
949
|
+
if (value === false) return "false";
|
|
950
|
+
if (typeof value === "number") {
|
|
951
|
+
if (!Number.isSafeInteger(value) || Object.is(value, -0)) {
|
|
952
|
+
throw new SignedObjectError(`not an integer in range: ${value}`);
|
|
953
|
+
}
|
|
954
|
+
return String(value);
|
|
955
|
+
}
|
|
956
|
+
if (typeof value === "string") {
|
|
957
|
+
checkString(value);
|
|
958
|
+
return '"' + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
959
|
+
}
|
|
960
|
+
if (Array.isArray(value)) return "[" + value.map(encode).join(",") + "]";
|
|
961
|
+
if (isPlainObject(value)) {
|
|
962
|
+
return "{" + Object.keys(value).sort().map((key) => {
|
|
963
|
+
checkString(key);
|
|
964
|
+
return encode(key) + ":" + encode(value[key]);
|
|
965
|
+
}).join(",") + "}";
|
|
966
|
+
}
|
|
967
|
+
if (value === null) throw new SignedObjectError("null is not allowed");
|
|
968
|
+
throw new SignedObjectError(`not allowed: ${typeof value}`);
|
|
969
|
+
}
|
|
970
|
+
function canonical(body) {
|
|
971
|
+
if (!isPlainObject(body)) throw new SignedObjectError("a body must be a JSON object");
|
|
972
|
+
return new TextEncoder().encode(encode(body));
|
|
973
|
+
}
|
|
974
|
+
function hashCanonical(body) {
|
|
975
|
+
return createHash2("sha256").update(`UAM-${String(body["type"])}-v1
|
|
976
|
+
`).update(canonical(body)).digest("base64url");
|
|
977
|
+
}
|
|
978
|
+
function objectHash(body) {
|
|
979
|
+
validate(body);
|
|
980
|
+
return hashCanonical(body);
|
|
981
|
+
}
|
|
982
|
+
var TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/;
|
|
983
|
+
var HASH_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
984
|
+
var OWNER_ID_RE = /^[A-Za-z0-9_-]{22}$/;
|
|
985
|
+
var NONCE_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
986
|
+
var SECRET_RE = /^[A-Za-z0-9_-]{22}$/;
|
|
987
|
+
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])?)+$/;
|
|
988
|
+
function fail(path, why) {
|
|
989
|
+
throw new SignedObjectError(`${path}: ${why}`);
|
|
990
|
+
}
|
|
991
|
+
var string_ = (v, p) => {
|
|
992
|
+
if (typeof v !== "string" || v.length === 0) fail(p, "must be a non-empty string");
|
|
993
|
+
};
|
|
994
|
+
var label = (v, p) => {
|
|
995
|
+
string_(v, p);
|
|
996
|
+
if (v.length > 64) fail(p, "at most 64 characters");
|
|
997
|
+
if (v.includes('"')) fail(p, 'must not contain "');
|
|
998
|
+
};
|
|
999
|
+
var matching = (re, what) => (v, p) => {
|
|
1000
|
+
if (typeof v !== "string" || !re.test(v)) fail(p, `must be ${what}`);
|
|
1001
|
+
};
|
|
1002
|
+
var hash = matching(HASH_RE, "a hash (43 base64url characters)");
|
|
1003
|
+
var ownerId = matching(OWNER_ID_RE, "an owner id (22 base64url characters)");
|
|
1004
|
+
var nonce = matching(NONCE_RE, "a nonce (16-64 base64url characters)");
|
|
1005
|
+
var secret = matching(SECRET_RE, "a secret (22 base64url characters)");
|
|
1006
|
+
var domain = matching(DOMAIN_RE, "a lowercase domain");
|
|
1007
|
+
function parseTime(value) {
|
|
1008
|
+
const m = TIME_RE.exec(value);
|
|
1009
|
+
if (!m) return null;
|
|
1010
|
+
const [y, mo, d, h, mi, s] = m.slice(1).map(Number);
|
|
1011
|
+
const t = Date.UTC(y, mo - 1, d, h, mi, s);
|
|
1012
|
+
const back = new Date(t);
|
|
1013
|
+
if (back.getUTCFullYear() !== y || back.getUTCMonth() !== mo - 1 || back.getUTCDate() !== d || back.getUTCHours() !== h || back.getUTCMinutes() !== mi || back.getUTCSeconds() !== s) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
return t;
|
|
1017
|
+
}
|
|
1018
|
+
var time = (v, p) => {
|
|
1019
|
+
if (typeof v !== "string" || !TIME_RE.test(v)) fail(p, "must be YYYY-MM-DDTHH:MM:SSZ");
|
|
1020
|
+
if (parseTime(v) === null) fail(p, "not a real time");
|
|
1021
|
+
};
|
|
1022
|
+
var bool = (v, p) => {
|
|
1023
|
+
if (typeof v !== "boolean") fail(p, "must be true or false");
|
|
1024
|
+
};
|
|
1025
|
+
var constant = (expected) => (v, p) => {
|
|
1026
|
+
if (v !== expected) fail(p, `must be ${JSON.stringify(expected)}`);
|
|
1027
|
+
};
|
|
1028
|
+
var oneOf = (...options) => (v, p) => {
|
|
1029
|
+
if (typeof v !== "string" || !options.includes(v)) fail(p, `must be one of ${options.join(", ")}`);
|
|
1030
|
+
};
|
|
1031
|
+
var uint = (v, p) => {
|
|
1032
|
+
if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 0) {
|
|
1033
|
+
fail(p, "must be a non-negative integer");
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
var address = (v, p) => {
|
|
1037
|
+
if (typeof v !== "string") fail(p, "must be an address");
|
|
1038
|
+
let full;
|
|
1039
|
+
try {
|
|
1040
|
+
full = parseAddress(v).full;
|
|
1041
|
+
} catch {
|
|
1042
|
+
return fail(p, "must be an address");
|
|
1043
|
+
}
|
|
1044
|
+
if (full !== v) fail(p, "must be an address in canonical (lowercase) form");
|
|
1045
|
+
};
|
|
1046
|
+
var did = (kind) => (v, p) => {
|
|
1047
|
+
let found;
|
|
1048
|
+
try {
|
|
1049
|
+
found = decodeDidKey(v).kind;
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
return fail(p, err instanceof Error ? err.message : String(err));
|
|
1052
|
+
}
|
|
1053
|
+
if (found !== kind) fail(p, `must be a ${kind} did:key`);
|
|
1054
|
+
};
|
|
1055
|
+
var list = (item, minLen = 1, maxLen = 64) => (v, p) => {
|
|
1056
|
+
if (!Array.isArray(v) || v.length < minLen || v.length > maxLen) {
|
|
1057
|
+
fail(p, `must be a list of ${minLen}-${maxLen} items`);
|
|
1058
|
+
}
|
|
1059
|
+
v.forEach((entry, i) => item(entry, `${p}[${i}]`));
|
|
1060
|
+
};
|
|
1061
|
+
var object = (required, optional = {}) => (v, p) => {
|
|
1062
|
+
if (!isPlainObject(v)) fail(p, "must be an object");
|
|
1063
|
+
for (const key of Object.keys(v)) {
|
|
1064
|
+
if (!has(required, key) && !has(optional, key)) fail(`${p}.${key}`, "unknown member");
|
|
1065
|
+
}
|
|
1066
|
+
for (const [key, member] of Object.entries(required)) {
|
|
1067
|
+
if (!has(v, key)) fail(`${p}.${key}`, "missing");
|
|
1068
|
+
member(v[key], `${p}.${key}`);
|
|
1069
|
+
}
|
|
1070
|
+
for (const [key, member] of Object.entries(optional)) {
|
|
1071
|
+
if (has(v, key)) member(v[key], `${p}.${key}`);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
var location = (v, p) => {
|
|
1075
|
+
if (v !== "shared") address(v, p);
|
|
1076
|
+
};
|
|
1077
|
+
var access = object({ actions: list(oneOf("message"), 1, 1), locations: list(location) });
|
|
1078
|
+
var ownerRef = object({ relay: domain, ownerId, keysHash: hash });
|
|
1079
|
+
var ownerRefNoKeys = object({ relay: domain, ownerId });
|
|
1080
|
+
var passkey = did("p256");
|
|
1081
|
+
var agentKey = did("ed25519");
|
|
1082
|
+
var keysOwner = object({ relay: domain }, { ownerId });
|
|
1083
|
+
var EXT = { ext: () => void 0, crit: () => void 0 };
|
|
1084
|
+
function checkExt(body) {
|
|
1085
|
+
if (has(body, "ext") && !isPlainObject(body["ext"])) fail("ext", "must be an object");
|
|
1086
|
+
if (has(body, "crit")) {
|
|
1087
|
+
const crit = body["crit"];
|
|
1088
|
+
if (!has(body, "ext")) fail("crit", "needs ext");
|
|
1089
|
+
if (!Array.isArray(crit) || !crit.every((k) => typeof k === "string")) {
|
|
1090
|
+
fail("crit", "must be a list of ext keys");
|
|
1091
|
+
}
|
|
1092
|
+
for (const key of crit) {
|
|
1093
|
+
if (!has(body["ext"], key)) fail(`crit.${key}`, "not in ext");
|
|
1094
|
+
fail(`crit.${key}`, "unknown critical extension");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
function span(body, from, to) {
|
|
1099
|
+
return parseTime(body[to]) - parseTime(body[from]);
|
|
1100
|
+
}
|
|
1101
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
1102
|
+
var SCHEMAS = {
|
|
1103
|
+
UamOwnerKeys(body) {
|
|
1104
|
+
object(
|
|
1105
|
+
{
|
|
1106
|
+
type: constant("UamOwnerKeys"),
|
|
1107
|
+
v: constant(1),
|
|
1108
|
+
owner: keysOwner,
|
|
1109
|
+
seq: uint,
|
|
1110
|
+
keys: list(object({ id: passkey, rpId: domain, backupEligible: bool, label }), 1, 16)
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
prev: hash,
|
|
1114
|
+
removed: list(object({ id: passkey, reason: oneOf("removed", "compromised") }), 1, 64),
|
|
1115
|
+
recovery: bool,
|
|
1116
|
+
...EXT
|
|
1117
|
+
}
|
|
1118
|
+
)(body, "$");
|
|
1119
|
+
const owner = body["owner"];
|
|
1120
|
+
if (body["seq"] === 0) {
|
|
1121
|
+
if (has(owner, "ownerId") || has(body, "prev")) fail("$", "entry 0 has no owner.ownerId and no prev");
|
|
1122
|
+
} else if (!has(owner, "ownerId") || !has(body, "prev")) {
|
|
1123
|
+
fail("$", "a later entry needs owner.ownerId and prev");
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
UamOwnerLink(body) {
|
|
1127
|
+
object({
|
|
1128
|
+
type: constant("UamOwnerLink"),
|
|
1129
|
+
v: constant(1),
|
|
1130
|
+
owner: ownerRef,
|
|
1131
|
+
agent: address,
|
|
1132
|
+
agentKey,
|
|
1133
|
+
relay: domain,
|
|
1134
|
+
via: oneOf("setup", "link"),
|
|
1135
|
+
shared: bool,
|
|
1136
|
+
effectiveAt: time,
|
|
1137
|
+
validFrom: time,
|
|
1138
|
+
nonce
|
|
1139
|
+
}, { ...EXT })(body, "$");
|
|
1140
|
+
},
|
|
1141
|
+
UamOwnerUnlink(body) {
|
|
1142
|
+
object({
|
|
1143
|
+
type: constant("UamOwnerUnlink"),
|
|
1144
|
+
v: constant(1),
|
|
1145
|
+
owner: ownerRef,
|
|
1146
|
+
agent: address,
|
|
1147
|
+
agentKey,
|
|
1148
|
+
reason: oneOf("owner", "admin", "relink", "authority", "lapsed"),
|
|
1149
|
+
validFrom: time,
|
|
1150
|
+
nonce
|
|
1151
|
+
}, { ...EXT })(body, "$");
|
|
1152
|
+
},
|
|
1153
|
+
UamInvite(body) {
|
|
1154
|
+
object(
|
|
1155
|
+
{
|
|
1156
|
+
type: constant("UamInvite"),
|
|
1157
|
+
v: constant(1),
|
|
1158
|
+
inviter: ownerRef,
|
|
1159
|
+
offer: access,
|
|
1160
|
+
ask: access,
|
|
1161
|
+
secretHash: hash,
|
|
1162
|
+
mode: oneOf("preapproved", "confirm"),
|
|
1163
|
+
maxUses: constant(1),
|
|
1164
|
+
validFrom: time,
|
|
1165
|
+
redeemBy: time,
|
|
1166
|
+
nonce
|
|
1167
|
+
},
|
|
1168
|
+
{ emailHash: hash, ...EXT }
|
|
1169
|
+
)(body, "$");
|
|
1170
|
+
if (body["mode"] === "preapproved" && !has(body, "emailHash")) {
|
|
1171
|
+
fail("$.emailHash", "a pre-approved invite names the invitee's email");
|
|
1172
|
+
}
|
|
1173
|
+
const d = span(body, "validFrom", "redeemBy");
|
|
1174
|
+
if (!(d > 0 && d <= INVITE_MAX_DAYS * DAY)) {
|
|
1175
|
+
fail("$.redeemBy", `must be after validFrom, within ${INVITE_MAX_DAYS} days`);
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
UamRedemption(body) {
|
|
1179
|
+
object({
|
|
1180
|
+
type: constant("UamRedemption"),
|
|
1181
|
+
v: constant(1),
|
|
1182
|
+
invite: hash,
|
|
1183
|
+
secret,
|
|
1184
|
+
invitee: ownerRef,
|
|
1185
|
+
issuer: passkey,
|
|
1186
|
+
createdAt: time,
|
|
1187
|
+
nonce
|
|
1188
|
+
}, { ...EXT })(body, "$");
|
|
1189
|
+
},
|
|
1190
|
+
UamContactGrant(body) {
|
|
1191
|
+
object({
|
|
1192
|
+
type: constant("UamContactGrant"),
|
|
1193
|
+
v: constant(1),
|
|
1194
|
+
issuer: passkey,
|
|
1195
|
+
issuerOwner: ownerRefNoKeys,
|
|
1196
|
+
credentialSubject: subject,
|
|
1197
|
+
authorization_details: list(object({
|
|
1198
|
+
type: constant("uam_contact"),
|
|
1199
|
+
actions: list(oneOf("message"), 1, 1),
|
|
1200
|
+
locations: list(location)
|
|
1201
|
+
}), 1, 8),
|
|
1202
|
+
via: oneOf("intro_email", "intro_code", "intro_qr", "request", "carried_over"),
|
|
1203
|
+
enforcer: domain,
|
|
1204
|
+
validFrom: time,
|
|
1205
|
+
validUntil: time,
|
|
1206
|
+
nonce
|
|
1207
|
+
}, { ...EXT })(body, "$");
|
|
1208
|
+
const d = span(body, "validFrom", "validUntil");
|
|
1209
|
+
if (!(d > 0 && d <= GRANT_MAX_DAYS * DAY)) {
|
|
1210
|
+
fail("$.validUntil", `must be after validFrom, within ${GRANT_MAX_DAYS} days`);
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
// Who may message an agent, as its owner approved it (P2e). Only a change
|
|
1214
|
+
// that widens it is signed; narrowing needs no passkey.
|
|
1215
|
+
UamAgentPolicy(body) {
|
|
1216
|
+
object({
|
|
1217
|
+
type: constant("UamAgentPolicy"),
|
|
1218
|
+
v: constant(1),
|
|
1219
|
+
owner: ownerRef,
|
|
1220
|
+
agent: address,
|
|
1221
|
+
agentKey,
|
|
1222
|
+
policy: oneOf("open", "contacts"),
|
|
1223
|
+
sameOwner: bool,
|
|
1224
|
+
validFrom: time,
|
|
1225
|
+
nonce
|
|
1226
|
+
}, { ...EXT })(body, "$");
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
var SUBJECTS = {
|
|
1230
|
+
owner: object({ kind: constant("owner"), owner: ownerRef }),
|
|
1231
|
+
invitee: object({ kind: constant("invitee"), invite: hash }),
|
|
1232
|
+
agent: object({ kind: constant("agent"), id: agentKey, address })
|
|
1233
|
+
};
|
|
1234
|
+
function subject(value, path) {
|
|
1235
|
+
const kind = isPlainObject(value) ? value["kind"] : void 0;
|
|
1236
|
+
if (typeof kind !== "string" || !has(SUBJECTS, kind)) {
|
|
1237
|
+
fail(`${path}.kind`, `must be one of ${Object.keys(SUBJECTS).join(", ")}`);
|
|
1238
|
+
}
|
|
1239
|
+
SUBJECTS[kind](value, path);
|
|
1240
|
+
}
|
|
1241
|
+
function validate(body) {
|
|
1242
|
+
if (!isPlainObject(body)) throw new SignedObjectError("a body must be a JSON object");
|
|
1243
|
+
encode(body);
|
|
1244
|
+
const type = body["type"];
|
|
1245
|
+
if (typeof type !== "string" || !has(SCHEMAS, type)) {
|
|
1246
|
+
throw new SignedObjectError(`unknown type: ${JSON.stringify(type)}`);
|
|
1247
|
+
}
|
|
1248
|
+
SCHEMAS[type](body);
|
|
1249
|
+
checkExt(body);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/protocol/webauthn.ts
|
|
1253
|
+
import { createHash as createHash3 } from "crypto";
|
|
1254
|
+
import { p256 } from "@noble/curves/p256";
|
|
1255
|
+
var ES256 = -7;
|
|
1256
|
+
var UP = 1;
|
|
1257
|
+
var UV = 4;
|
|
1258
|
+
var BE = 8;
|
|
1259
|
+
var AT = 64;
|
|
1260
|
+
var ED = 128;
|
|
1261
|
+
var WebAuthnError = class extends UAMError {
|
|
1262
|
+
constructor(message) {
|
|
1263
|
+
super(message);
|
|
1264
|
+
this.name = "WebAuthnError";
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
function credentialDid(credential) {
|
|
1268
|
+
return encodeDidKey("p256", credential.publicKey);
|
|
1269
|
+
}
|
|
1270
|
+
var sha256 = (...parts) => {
|
|
1271
|
+
const h = createHash3("sha256");
|
|
1272
|
+
for (const part of parts) h.update(part);
|
|
1273
|
+
return new Uint8Array(h.digest());
|
|
1274
|
+
};
|
|
1275
|
+
var equal = (a, b) => a.length === b.length && a.every((byte, i) => byte === b[i]);
|
|
1276
|
+
function cbor(data, start) {
|
|
1277
|
+
let i = start;
|
|
1278
|
+
if (i >= data.length) throw new WebAuthnError("CBOR: truncated");
|
|
1279
|
+
const first = data[i];
|
|
1280
|
+
const major = first >> 5;
|
|
1281
|
+
const info = first & 31;
|
|
1282
|
+
i += 1;
|
|
1283
|
+
if (major === 7) {
|
|
1284
|
+
if (info === 20) return [false, i];
|
|
1285
|
+
if (info === 21) return [true, i];
|
|
1286
|
+
throw new WebAuthnError("CBOR: unsupported simple value or float");
|
|
1287
|
+
}
|
|
1288
|
+
let n;
|
|
1289
|
+
if (info < 24) {
|
|
1290
|
+
n = info;
|
|
1291
|
+
} else if (info >= 24 && info <= 27) {
|
|
1292
|
+
const size = 1 << info - 24;
|
|
1293
|
+
if (i + size > data.length) throw new WebAuthnError("CBOR: truncated");
|
|
1294
|
+
let big = 0n;
|
|
1295
|
+
for (let k = 0; k < size; k += 1) big = big << 8n | BigInt(data[i + k]);
|
|
1296
|
+
if (big > BigInt(Number.MAX_SAFE_INTEGER)) throw new WebAuthnError("CBOR: number too large");
|
|
1297
|
+
n = Number(big);
|
|
1298
|
+
i += size;
|
|
1299
|
+
} else {
|
|
1300
|
+
throw new WebAuthnError("CBOR: indefinite lengths are not supported");
|
|
1301
|
+
}
|
|
1302
|
+
if (major === 0) return [n, i];
|
|
1303
|
+
if (major === 1) return [-1 - n, i];
|
|
1304
|
+
if (major === 2 || major === 3) {
|
|
1305
|
+
if (i + n > data.length) throw new WebAuthnError("CBOR: truncated");
|
|
1306
|
+
const raw = data.slice(i, i + n);
|
|
1307
|
+
i += n;
|
|
1308
|
+
if (major === 2) return [raw, i];
|
|
1309
|
+
try {
|
|
1310
|
+
return [new TextDecoder("utf-8", { fatal: true }).decode(raw), i];
|
|
1311
|
+
} catch {
|
|
1312
|
+
throw new WebAuthnError("CBOR: text is not UTF-8");
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (major === 4) {
|
|
1316
|
+
const items = [];
|
|
1317
|
+
for (let k = 0; k < n; k += 1) {
|
|
1318
|
+
const [item, next] = cbor(data, i);
|
|
1319
|
+
items.push(item);
|
|
1320
|
+
i = next;
|
|
1321
|
+
}
|
|
1322
|
+
return [items, i];
|
|
1323
|
+
}
|
|
1324
|
+
if (major === 5) {
|
|
1325
|
+
const out = /* @__PURE__ */ new Map();
|
|
1326
|
+
for (let k = 0; k < n; k += 1) {
|
|
1327
|
+
const [key, afterKey] = cbor(data, i);
|
|
1328
|
+
if (typeof key !== "number" && typeof key !== "string") {
|
|
1329
|
+
throw new WebAuthnError("CBOR: map keys must be integers or text");
|
|
1330
|
+
}
|
|
1331
|
+
if (out.has(key)) throw new WebAuthnError("CBOR: duplicate map key");
|
|
1332
|
+
const [value, afterValue] = cbor(data, afterKey);
|
|
1333
|
+
out.set(key, value);
|
|
1334
|
+
i = afterValue;
|
|
1335
|
+
}
|
|
1336
|
+
return [out, i];
|
|
1337
|
+
}
|
|
1338
|
+
throw new WebAuthnError("CBOR: tags are not supported");
|
|
1339
|
+
}
|
|
1340
|
+
function cborWhole(data) {
|
|
1341
|
+
const [value, end] = cbor(data, 0);
|
|
1342
|
+
if (end !== data.length) throw new WebAuthnError("CBOR: trailing bytes");
|
|
1343
|
+
return value;
|
|
1344
|
+
}
|
|
1345
|
+
function checkClientData(raw, kind, challenge, origin) {
|
|
1346
|
+
let data;
|
|
1347
|
+
try {
|
|
1348
|
+
data = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw));
|
|
1349
|
+
} catch {
|
|
1350
|
+
throw new WebAuthnError("clientDataJSON is not JSON");
|
|
1351
|
+
}
|
|
1352
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
1353
|
+
throw new WebAuthnError("clientDataJSON is not an object");
|
|
1354
|
+
}
|
|
1355
|
+
const d = data;
|
|
1356
|
+
if (d["type"] !== kind) throw new WebAuthnError(`clientData.type must be ${kind}`);
|
|
1357
|
+
if (d["challenge"] !== Buffer.from(challenge).toString("base64url")) {
|
|
1358
|
+
throw new WebAuthnError("clientData.challenge doesn't match");
|
|
1359
|
+
}
|
|
1360
|
+
if (d["origin"] !== origin) throw new WebAuthnError("clientData.origin doesn't match");
|
|
1361
|
+
if ("crossOrigin" in d && d["crossOrigin"] !== false) {
|
|
1362
|
+
throw new WebAuthnError("cross-origin ceremonies are refused");
|
|
1363
|
+
}
|
|
1364
|
+
if ("topOrigin" in d) throw new WebAuthnError("ceremonies in a frame are refused");
|
|
1365
|
+
}
|
|
1366
|
+
function checkAuthData(data, rpId) {
|
|
1367
|
+
if (data.length < 37) throw new WebAuthnError("authenticatorData too short");
|
|
1368
|
+
if (!equal(data.slice(0, 32), sha256(new TextEncoder().encode(rpId)))) {
|
|
1369
|
+
throw new WebAuthnError("rpIdHash doesn't match");
|
|
1370
|
+
}
|
|
1371
|
+
const flags = data[32];
|
|
1372
|
+
if (!(flags & UP)) throw new WebAuthnError("user not present");
|
|
1373
|
+
if (!(flags & UV)) throw new WebAuthnError("user not verified");
|
|
1374
|
+
const count = new DataView(data.buffer, data.byteOffset + 33, 4).getUint32(0, false);
|
|
1375
|
+
return { flags, count };
|
|
1376
|
+
}
|
|
1377
|
+
function point(bytes) {
|
|
1378
|
+
try {
|
|
1379
|
+
return p256.ProjectivePoint.fromHex(bytes);
|
|
1380
|
+
} catch {
|
|
1381
|
+
throw new WebAuthnError("not a P-256 point");
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
function verifyRegistration(args) {
|
|
1385
|
+
checkClientData(args.clientDataJSON, "webauthn.create", args.challenge, args.origin);
|
|
1386
|
+
const att = cborWhole(args.attestationObject);
|
|
1387
|
+
if (!(att instanceof Map) || att.size !== 3 || !att.has("fmt") || !att.has("attStmt") || !att.has("authData")) {
|
|
1388
|
+
throw new WebAuthnError("attestationObject must have fmt, attStmt and authData");
|
|
1389
|
+
}
|
|
1390
|
+
const stmt = att.get("attStmt");
|
|
1391
|
+
if (att.get("fmt") !== "none" || !(stmt instanceof Map) || stmt.size !== 0) {
|
|
1392
|
+
throw new WebAuthnError("only attestation format 'none' is accepted");
|
|
1393
|
+
}
|
|
1394
|
+
const data = att.get("authData");
|
|
1395
|
+
if (!(data instanceof Uint8Array)) throw new WebAuthnError("authData must be bytes");
|
|
1396
|
+
const { flags, count } = checkAuthData(data, args.rpId);
|
|
1397
|
+
if (!(flags & AT)) throw new WebAuthnError("no attested credential data");
|
|
1398
|
+
if (data.length < 55) throw new WebAuthnError("attested credential data too short");
|
|
1399
|
+
const idLen = data[53] << 8 | data[54];
|
|
1400
|
+
const credentialId = data.slice(55, 55 + idLen);
|
|
1401
|
+
if (credentialId.length !== idLen) throw new WebAuthnError("credential id truncated");
|
|
1402
|
+
const [cose, end] = cbor(data, 55 + idLen);
|
|
1403
|
+
if (!(flags & ED) && end !== data.length) {
|
|
1404
|
+
throw new WebAuthnError("trailing bytes after the credential key");
|
|
1405
|
+
}
|
|
1406
|
+
if (!(cose instanceof Map) || cose.get(1) !== 2 || cose.get(3) !== ES256 || cose.get(-1) !== 1) {
|
|
1407
|
+
throw new WebAuthnError("only ES256 (P-256) passkeys are accepted");
|
|
1408
|
+
}
|
|
1409
|
+
const x = cose.get(-2);
|
|
1410
|
+
const y = cose.get(-3);
|
|
1411
|
+
if (!(x instanceof Uint8Array) || !(y instanceof Uint8Array) || x.length !== 32 || y.length !== 32) {
|
|
1412
|
+
throw new WebAuthnError("bad P-256 coordinates");
|
|
1413
|
+
}
|
|
1414
|
+
const key = point(new Uint8Array([4, ...x, ...y]));
|
|
1415
|
+
return {
|
|
1416
|
+
id: credentialId,
|
|
1417
|
+
alg: ES256,
|
|
1418
|
+
publicKey: key.toRawBytes(true),
|
|
1419
|
+
backupEligible: Boolean(flags & BE),
|
|
1420
|
+
signCount: count
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
function verifyAssertion(credential, args) {
|
|
1424
|
+
if (credential.alg !== ES256) throw new WebAuthnError("only ES256 (P-256) passkeys are accepted");
|
|
1425
|
+
checkClientData(args.clientDataJSON, "webauthn.get", args.challenge, args.origin);
|
|
1426
|
+
const { flags, count } = checkAuthData(args.authenticatorData, args.rpId);
|
|
1427
|
+
if (Boolean(flags & BE) !== credential.backupEligible) {
|
|
1428
|
+
throw new WebAuthnError("backup eligibility changed since registration");
|
|
1429
|
+
}
|
|
1430
|
+
const key = point(credential.publicKey);
|
|
1431
|
+
const digest = sha256(args.authenticatorData, sha256(args.clientDataJSON));
|
|
1432
|
+
let ok = false;
|
|
1433
|
+
try {
|
|
1434
|
+
ok = p256.verify(args.signature, digest, key.toRawBytes(false), {
|
|
1435
|
+
format: "der",
|
|
1436
|
+
lowS: false,
|
|
1437
|
+
prehash: false
|
|
1438
|
+
});
|
|
1439
|
+
} catch {
|
|
1440
|
+
ok = false;
|
|
1441
|
+
}
|
|
1442
|
+
if (!ok) throw new WebAuthnError("bad signature");
|
|
1443
|
+
const checkCounter = args.checkCounter ?? true;
|
|
1444
|
+
if (checkCounter && (count || credential.signCount) && count <= credential.signCount) {
|
|
1445
|
+
throw new WebAuthnError("sign count didn't go up: the passkey may have been cloned");
|
|
1446
|
+
}
|
|
1447
|
+
return count;
|
|
1448
|
+
}
|
|
1449
|
+
function approvalChallenge(items, screen) {
|
|
1450
|
+
if (items.length === 0) throw new WebAuthnError("an approval covers at least one object");
|
|
1451
|
+
const body = {
|
|
1452
|
+
set: items.map(([t, h]) => ({ t, h })),
|
|
1453
|
+
screen: createHash3("sha256").update(screen, "utf8").digest("hex")
|
|
1454
|
+
};
|
|
1455
|
+
let data;
|
|
1456
|
+
try {
|
|
1457
|
+
data = canonical(body);
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
if (err instanceof SignedObjectError) throw new WebAuthnError(`bad approval set: ${err.message}`);
|
|
1460
|
+
throw err;
|
|
1461
|
+
}
|
|
1462
|
+
return sha256(new TextEncoder().encode("UAM-APPROVAL-v1\n"), data);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
669
1465
|
// src/version.ts
|
|
670
|
-
var SDK_VERSION = "0.
|
|
1466
|
+
var SDK_VERSION = "0.4.0";
|
|
671
1467
|
|
|
672
1468
|
// src/sdk/config.ts
|
|
673
1469
|
import { homedir } from "os";
|
|
@@ -843,11 +1639,11 @@ var KeyManager = class {
|
|
|
843
1639
|
/**
|
|
844
1640
|
* Store the address `relayUrl` gave this agent.
|
|
845
1641
|
*/
|
|
846
|
-
saveAddress(name,
|
|
1642
|
+
saveAddress(name, address2, relayUrl) {
|
|
847
1643
|
mkdirSync(this._keyDir, { recursive: true });
|
|
848
1644
|
writeFileSync(
|
|
849
1645
|
join2(this._keyDir, `${name}.address`),
|
|
850
|
-
JSON.stringify({ address, relay_url: relayUrl })
|
|
1646
|
+
JSON.stringify({ address: address2, relay_url: relayUrl })
|
|
851
1647
|
);
|
|
852
1648
|
}
|
|
853
1649
|
/**
|
|
@@ -860,14 +1656,14 @@ var KeyManager = class {
|
|
|
860
1656
|
try {
|
|
861
1657
|
const path = join2(this._keyDir, `${name}.address`);
|
|
862
1658
|
const saved = JSON.parse(readFileSync(path, "utf-8"));
|
|
863
|
-
const
|
|
1659
|
+
const address2 = saved?.address;
|
|
864
1660
|
const savedRelay = saved?.relay_url;
|
|
865
|
-
if (typeof
|
|
1661
|
+
if (typeof address2 !== "string" || typeof savedRelay !== "string") {
|
|
866
1662
|
return null;
|
|
867
1663
|
}
|
|
868
1664
|
if (trimRelayUrl(savedRelay) !== trimRelayUrl(relayUrl)) return null;
|
|
869
|
-
parseAddress(
|
|
870
|
-
return
|
|
1665
|
+
parseAddress(address2);
|
|
1666
|
+
return address2;
|
|
871
1667
|
} catch {
|
|
872
1668
|
return null;
|
|
873
1669
|
}
|
|
@@ -935,6 +1731,42 @@ var KeyManager = class {
|
|
|
935
1731
|
}
|
|
936
1732
|
};
|
|
937
1733
|
|
|
1734
|
+
// src/sdk/message.ts
|
|
1735
|
+
var SEND_STATUSES = [
|
|
1736
|
+
"delivered",
|
|
1737
|
+
"stored",
|
|
1738
|
+
"relayed",
|
|
1739
|
+
"queued",
|
|
1740
|
+
"duplicate",
|
|
1741
|
+
"dropped",
|
|
1742
|
+
"pending",
|
|
1743
|
+
"accepted",
|
|
1744
|
+
"unknown"
|
|
1745
|
+
];
|
|
1746
|
+
function sendStatus(answer) {
|
|
1747
|
+
if (answer === null) return "unknown";
|
|
1748
|
+
const status = answer["status"];
|
|
1749
|
+
if (typeof status === "string" && SEND_STATUSES.includes(status) && status !== "accepted" && status !== "unknown") {
|
|
1750
|
+
return status;
|
|
1751
|
+
}
|
|
1752
|
+
return "accepted";
|
|
1753
|
+
}
|
|
1754
|
+
function supports(features, name, version = 1) {
|
|
1755
|
+
const prefix = `${name}:`;
|
|
1756
|
+
for (const feature of features) {
|
|
1757
|
+
if (feature === name && version <= 1) return true;
|
|
1758
|
+
if (feature.startsWith(prefix)) {
|
|
1759
|
+
const rest = feature.slice(prefix.length);
|
|
1760
|
+
if (/^\d+$/.test(rest) && Number(rest) >= version) return true;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return false;
|
|
1764
|
+
}
|
|
1765
|
+
var PENDING_NOTE = "Not delivered: {to} only accepts messages from approved contacts. Do not resend it.";
|
|
1766
|
+
function pendingNote(to) {
|
|
1767
|
+
return PENDING_NOTE.replace("{to}", to);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
938
1770
|
// src/sdk/contact-book.ts
|
|
939
1771
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
940
1772
|
import { join as join3, dirname } from "path";
|
|
@@ -1032,36 +1864,36 @@ var ContactBook = class {
|
|
|
1032
1864
|
/**
|
|
1033
1865
|
* Check if an address is in the contact book (in-memory, no I/O).
|
|
1034
1866
|
*/
|
|
1035
|
-
isKnown(
|
|
1036
|
-
return this._knownAddresses.has(
|
|
1867
|
+
isKnown(address2) {
|
|
1868
|
+
return this._knownAddresses.has(address2);
|
|
1037
1869
|
}
|
|
1038
1870
|
/**
|
|
1039
1871
|
* Check if an address matches any blocked pattern (O(1) set lookup).
|
|
1040
1872
|
*/
|
|
1041
|
-
isBlocked(
|
|
1042
|
-
if (this._blockedExact.has(
|
|
1873
|
+
isBlocked(address2) {
|
|
1874
|
+
if (this._blockedExact.has(address2)) {
|
|
1043
1875
|
return true;
|
|
1044
1876
|
}
|
|
1045
|
-
if (
|
|
1046
|
-
const
|
|
1047
|
-
return this._blockedDomains.has(
|
|
1877
|
+
if (address2.includes("::")) {
|
|
1878
|
+
const domain2 = address2.split("::")[1];
|
|
1879
|
+
return this._blockedDomains.has(domain2);
|
|
1048
1880
|
}
|
|
1049
1881
|
return false;
|
|
1050
1882
|
}
|
|
1051
1883
|
/**
|
|
1052
1884
|
* Look up the public key for a known contact.
|
|
1053
1885
|
*/
|
|
1054
|
-
getPublicKey(
|
|
1886
|
+
getPublicKey(address2) {
|
|
1055
1887
|
if (this._db === null) return null;
|
|
1056
|
-
const row = this._db.prepare("SELECT public_key FROM contacts WHERE address = ?").get(
|
|
1888
|
+
const row = this._db.prepare("SELECT public_key FROM contacts WHERE address = ?").get(address2);
|
|
1057
1889
|
return row?.public_key ?? null;
|
|
1058
1890
|
}
|
|
1059
1891
|
/**
|
|
1060
1892
|
* Return the relay URLs for a known contact (CARD-04).
|
|
1061
1893
|
*/
|
|
1062
|
-
getRelayUrls(
|
|
1894
|
+
getRelayUrls(address2) {
|
|
1063
1895
|
if (this._db === null) return null;
|
|
1064
|
-
const row = this._db.prepare("SELECT relay, relays_json FROM contacts WHERE address = ?").get(
|
|
1896
|
+
const row = this._db.prepare("SELECT relay, relays_json FROM contacts WHERE address = ?").get(address2);
|
|
1065
1897
|
if (!row) return null;
|
|
1066
1898
|
if (row.relays_json !== null) {
|
|
1067
1899
|
return JSON.parse(row.relays_json);
|
|
@@ -1082,19 +1914,19 @@ var ContactBook = class {
|
|
|
1082
1914
|
* @throws {KeyPinningError} If a pinned/verified contact's key would be
|
|
1083
1915
|
* replaced by a different key without `force`.
|
|
1084
1916
|
*/
|
|
1085
|
-
addContact(
|
|
1917
|
+
addContact(address2, publicKey, options) {
|
|
1086
1918
|
if (this._db === null) {
|
|
1087
1919
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1088
1920
|
}
|
|
1089
|
-
const existing = this._db.prepare("SELECT public_key, trust_state FROM contacts WHERE address = ?").get(
|
|
1921
|
+
const existing = this._db.prepare("SELECT public_key, trust_state FROM contacts WHERE address = ?").get(address2);
|
|
1090
1922
|
if (existing !== void 0 && (existing.trust_state === "pinned" || existing.trust_state === "verified") && existing.public_key !== publicKey) {
|
|
1091
1923
|
if (!options?.force) {
|
|
1092
1924
|
throw new KeyPinningError(
|
|
1093
|
-
`Refusing to overwrite ${existing.trust_state} contact ${JSON.stringify(
|
|
1925
|
+
`Refusing to overwrite ${existing.trust_state} contact ${JSON.stringify(address2)}: stored public key does not match the new key. Pass force: true to rotate.`
|
|
1094
1926
|
);
|
|
1095
1927
|
}
|
|
1096
1928
|
log.warn(
|
|
1097
|
-
`Force-replacing the key of ${existing.trust_state} contact ${
|
|
1929
|
+
`Force-replacing the key of ${existing.trust_state} contact ${address2} (key rotation)`
|
|
1098
1930
|
);
|
|
1099
1931
|
}
|
|
1100
1932
|
const displayName = options?.displayName ?? null;
|
|
@@ -1113,8 +1945,8 @@ var ContactBook = class {
|
|
|
1113
1945
|
relay = COALESCE(excluded.relay, contacts.relay),
|
|
1114
1946
|
relays_json = COALESCE(excluded.relays_json, contacts.relays_json),
|
|
1115
1947
|
last_seen = datetime('now')`
|
|
1116
|
-
).run(
|
|
1117
|
-
this._knownAddresses.add(
|
|
1948
|
+
).run(address2, publicKey, displayName, trustState, trustSource, relay, relaysJson);
|
|
1949
|
+
this._knownAddresses.add(address2);
|
|
1118
1950
|
}
|
|
1119
1951
|
/**
|
|
1120
1952
|
* Return all contacts sorted by last_seen DESC.
|
|
@@ -1135,13 +1967,13 @@ var ContactBook = class {
|
|
|
1135
1967
|
/**
|
|
1136
1968
|
* Store a pending handshake request.
|
|
1137
1969
|
*/
|
|
1138
|
-
addPending(
|
|
1970
|
+
addPending(address2, contactCardJson) {
|
|
1139
1971
|
if (this._db === null) {
|
|
1140
1972
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1141
1973
|
}
|
|
1142
1974
|
this._db.prepare(
|
|
1143
1975
|
`INSERT OR REPLACE INTO pending_handshakes (address, contact_card) VALUES (?, ?)`
|
|
1144
|
-
).run(
|
|
1976
|
+
).run(address2, contactCardJson);
|
|
1145
1977
|
}
|
|
1146
1978
|
/**
|
|
1147
1979
|
* Retrieve all pending handshake requests.
|
|
@@ -1160,9 +1992,9 @@ var ContactBook = class {
|
|
|
1160
1992
|
/**
|
|
1161
1993
|
* Remove a pending handshake request.
|
|
1162
1994
|
*/
|
|
1163
|
-
removePending(
|
|
1995
|
+
removePending(address2) {
|
|
1164
1996
|
if (this._db === null) return;
|
|
1165
|
-
this._db.prepare("DELETE FROM pending_handshakes WHERE address = ?").run(
|
|
1997
|
+
this._db.prepare("DELETE FROM pending_handshakes WHERE address = ?").run(address2);
|
|
1166
1998
|
}
|
|
1167
1999
|
/**
|
|
1168
2000
|
* Return pending handshakes older than N days.
|
|
@@ -1182,31 +2014,31 @@ var ContactBook = class {
|
|
|
1182
2014
|
/**
|
|
1183
2015
|
* Return the trust_state for an address, or null if unknown.
|
|
1184
2016
|
*/
|
|
1185
|
-
getTrustState(
|
|
2017
|
+
getTrustState(address2) {
|
|
1186
2018
|
if (this._db === null) return null;
|
|
1187
|
-
const row = this._db.prepare("SELECT trust_state FROM contacts WHERE address = ?").get(
|
|
2019
|
+
const row = this._db.prepare("SELECT trust_state FROM contacts WHERE address = ?").get(address2);
|
|
1188
2020
|
return row?.trust_state ?? null;
|
|
1189
2021
|
}
|
|
1190
2022
|
/**
|
|
1191
2023
|
* Set the pinned_at timestamp and trust_state='pinned' for a contact (TOFU).
|
|
1192
2024
|
*/
|
|
1193
|
-
setPinnedAt(
|
|
2025
|
+
setPinnedAt(address2) {
|
|
1194
2026
|
if (this._db === null) {
|
|
1195
2027
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1196
2028
|
}
|
|
1197
2029
|
this._db.prepare(
|
|
1198
2030
|
"UPDATE contacts SET pinned_at = datetime('now'), trust_state = 'pinned' WHERE address = ?"
|
|
1199
|
-
).run(
|
|
2031
|
+
).run(address2);
|
|
1200
2032
|
}
|
|
1201
2033
|
/**
|
|
1202
2034
|
* Remove a contact by address. Returns true if a contact was deleted.
|
|
1203
2035
|
*/
|
|
1204
|
-
removeContact(
|
|
2036
|
+
removeContact(address2) {
|
|
1205
2037
|
if (this._db === null) {
|
|
1206
2038
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1207
2039
|
}
|
|
1208
|
-
const result = this._db.prepare("DELETE FROM contacts WHERE address = ?").run(
|
|
1209
|
-
this._knownAddresses.delete(
|
|
2040
|
+
const result = this._db.prepare("DELETE FROM contacts WHERE address = ?").run(address2);
|
|
2041
|
+
this._knownAddresses.delete(address2);
|
|
1210
2042
|
return result.changes > 0;
|
|
1211
2043
|
}
|
|
1212
2044
|
/**
|
|
@@ -1214,8 +2046,8 @@ var ContactBook = class {
|
|
|
1214
2046
|
*
|
|
1215
2047
|
* Convenience helper for inbound message filtering (CARD-05, TOFU-02).
|
|
1216
2048
|
*/
|
|
1217
|
-
isTrustedOrVerified(
|
|
1218
|
-
const state = this.getTrustState(
|
|
2049
|
+
isTrustedOrVerified(address2) {
|
|
2050
|
+
const state = this.getTrustState(address2);
|
|
1219
2051
|
return state === "trusted" || state === "verified" || state === "pinned";
|
|
1220
2052
|
}
|
|
1221
2053
|
/**
|
|
@@ -1512,8 +2344,8 @@ var Tier3Resolver = class extends AddressResolver {
|
|
|
1512
2344
|
transport: http(rpcUrl)
|
|
1513
2345
|
});
|
|
1514
2346
|
}
|
|
1515
|
-
async resolvePublicKey(
|
|
1516
|
-
const parsed = parseAddress(
|
|
2347
|
+
async resolvePublicKey(address2, _token, _relayUrl) {
|
|
2348
|
+
const parsed = parseAddress(address2);
|
|
1517
2349
|
const name = parsed.domain;
|
|
1518
2350
|
const cached = this._cache.get(name);
|
|
1519
2351
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -1577,12 +2409,12 @@ var Tier3Resolver = class extends AddressResolver {
|
|
|
1577
2409
|
// src/sdk/resolver.ts
|
|
1578
2410
|
init_dns_verifier();
|
|
1579
2411
|
var Tier1Resolver = class extends AddressResolver {
|
|
1580
|
-
async resolvePublicKey(
|
|
2412
|
+
async resolvePublicKey(address2, _token, relayUrl) {
|
|
1581
2413
|
const resp = await fetch(
|
|
1582
|
-
`${relayUrl}/api/v1/agents/${
|
|
2414
|
+
`${relayUrl}/api/v1/agents/${address2}/public-key`
|
|
1583
2415
|
);
|
|
1584
2416
|
if (resp.status === 404) {
|
|
1585
|
-
throw new UAMError(`Agent not found: ${
|
|
2417
|
+
throw new UAMError(`Agent not found: ${address2}`);
|
|
1586
2418
|
}
|
|
1587
2419
|
if (!resp.ok) {
|
|
1588
2420
|
throw new UAMError(
|
|
@@ -1594,8 +2426,8 @@ var Tier1Resolver = class extends AddressResolver {
|
|
|
1594
2426
|
}
|
|
1595
2427
|
};
|
|
1596
2428
|
var Tier2Resolver = class extends AddressResolver {
|
|
1597
|
-
async resolvePublicKey(
|
|
1598
|
-
const parsed = parseAddress(
|
|
2429
|
+
async resolvePublicKey(address2, _token, _relayUrl) {
|
|
2430
|
+
const parsed = parseAddress(address2);
|
|
1599
2431
|
const txtRecords = await queryUamTxt(parsed.domain);
|
|
1600
2432
|
for (const txt of txtRecords) {
|
|
1601
2433
|
const tags = parseUamTxt(txt);
|
|
@@ -1607,7 +2439,7 @@ var Tier2Resolver = class extends AddressResolver {
|
|
|
1607
2439
|
const httpsKey = await resolveKeyViaHttps(parsed.agent, parsed.domain);
|
|
1608
2440
|
if (httpsKey) return httpsKey;
|
|
1609
2441
|
throw new UAMError(
|
|
1610
|
-
`Could not resolve public key for ${
|
|
2442
|
+
`Could not resolve public key for ${address2} via DNS or HTTPS`
|
|
1611
2443
|
);
|
|
1612
2444
|
}
|
|
1613
2445
|
};
|
|
@@ -1623,21 +2455,21 @@ var SmartResolver = class extends AddressResolver {
|
|
|
1623
2455
|
this._tier2 = new Tier2Resolver();
|
|
1624
2456
|
this._tier3 = tier3Config ? new Tier3Resolver(tier3Config) : null;
|
|
1625
2457
|
}
|
|
1626
|
-
async resolvePublicKey(
|
|
1627
|
-
const parsed = parseAddress(
|
|
1628
|
-
const
|
|
1629
|
-
if (
|
|
1630
|
-
return this._tier1.resolvePublicKey(
|
|
2458
|
+
async resolvePublicKey(address2, token, relayUrl) {
|
|
2459
|
+
const parsed = parseAddress(address2);
|
|
2460
|
+
const domain2 = parsed.domain;
|
|
2461
|
+
if (domain2 === this.relayDomain) {
|
|
2462
|
+
return this._tier1.resolvePublicKey(address2, token, relayUrl);
|
|
1631
2463
|
}
|
|
1632
|
-
if (
|
|
1633
|
-
return this._tier2.resolvePublicKey(
|
|
2464
|
+
if (domain2.includes(".")) {
|
|
2465
|
+
return this._tier2.resolvePublicKey(address2, token, relayUrl);
|
|
1634
2466
|
}
|
|
1635
2467
|
if (!this._tier3) {
|
|
1636
2468
|
throw new UAMError(
|
|
1637
|
-
`Tier 3 resolution requires contract config. Cannot resolve dot-free domain: '${
|
|
2469
|
+
`Tier 3 resolution requires contract config. Cannot resolve dot-free domain: '${domain2}'`
|
|
1638
2470
|
);
|
|
1639
2471
|
}
|
|
1640
|
-
return this._tier3.resolvePublicKey(
|
|
2472
|
+
return this._tier3.resolvePublicKey(address2, token, relayUrl);
|
|
1641
2473
|
}
|
|
1642
2474
|
};
|
|
1643
2475
|
|
|
@@ -1646,15 +2478,23 @@ var TransportBase = class {
|
|
|
1646
2478
|
};
|
|
1647
2479
|
|
|
1648
2480
|
// src/sdk/transport/http.ts
|
|
2481
|
+
async function readAnswer(resp) {
|
|
2482
|
+
try {
|
|
2483
|
+
const body = await resp.json();
|
|
2484
|
+
return body !== null && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
2485
|
+
} catch {
|
|
2486
|
+
return {};
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
1649
2489
|
var HTTPTransport = class extends TransportBase {
|
|
1650
2490
|
_relayUrl;
|
|
1651
2491
|
_token;
|
|
1652
2492
|
_address;
|
|
1653
|
-
constructor(relayUrl, token,
|
|
2493
|
+
constructor(relayUrl, token, address2) {
|
|
1654
2494
|
super();
|
|
1655
2495
|
this._relayUrl = relayUrl.replace(/\/+$/, "");
|
|
1656
2496
|
this._token = token;
|
|
1657
|
-
this._address =
|
|
2497
|
+
this._address = address2;
|
|
1658
2498
|
}
|
|
1659
2499
|
async connect() {
|
|
1660
2500
|
}
|
|
@@ -1674,6 +2514,7 @@ var HTTPTransport = class extends TransportBase {
|
|
|
1674
2514
|
`HTTP send failed: ${resp.status} ${resp.statusText}`
|
|
1675
2515
|
);
|
|
1676
2516
|
}
|
|
2517
|
+
return readAnswer(resp);
|
|
1677
2518
|
}
|
|
1678
2519
|
async receive(limit = 50) {
|
|
1679
2520
|
const url = new URL(
|
|
@@ -1706,6 +2547,7 @@ var BASE_DELAY = 1;
|
|
|
1706
2547
|
var MAX_DELAY = 60;
|
|
1707
2548
|
var JITTER_RANGE = 1;
|
|
1708
2549
|
var SEND_SETTLE_SECONDS = 5;
|
|
2550
|
+
var ACK_TIMEOUT_SECONDS = 5;
|
|
1709
2551
|
var WebSocketTransport = class extends TransportBase {
|
|
1710
2552
|
_url;
|
|
1711
2553
|
_token;
|
|
@@ -1721,8 +2563,12 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1721
2563
|
_unanswered = 0;
|
|
1722
2564
|
_settling = null;
|
|
1723
2565
|
_settled = null;
|
|
2566
|
+
/** Sends waiting for the relay's answer, by message_id. */
|
|
2567
|
+
_waiting = /* @__PURE__ */ new Map();
|
|
1724
2568
|
/** @internal How long a close waits, in ms. Replaceable in tests. */
|
|
1725
2569
|
_settleMs = SEND_SETTLE_SECONDS * 1e3;
|
|
2570
|
+
/** @internal How long send() waits for an answer, in ms. Replaceable in tests. */
|
|
2571
|
+
_ackMs = ACK_TIMEOUT_SECONDS * 1e3;
|
|
1726
2572
|
constructor(wsUrl, token, onMessage) {
|
|
1727
2573
|
super();
|
|
1728
2574
|
this._url = wsUrl;
|
|
@@ -1766,13 +2612,42 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1766
2612
|
this._ws = null;
|
|
1767
2613
|
}
|
|
1768
2614
|
this._connected = false;
|
|
2615
|
+
this._abandonWaiting();
|
|
1769
2616
|
}
|
|
2617
|
+
/**
|
|
2618
|
+
* Send an envelope and wait for the relay's answer.
|
|
2619
|
+
*
|
|
2620
|
+
* Resolves with the ack that names the message, or null when none came
|
|
2621
|
+
* within _ackMs or the socket went: the message may still have arrived.
|
|
2622
|
+
* Rejects with SendRefusedError when the relay refuses it by message_id
|
|
2623
|
+
* (relays before 1.1 don't name it, so their refusals end as null).
|
|
2624
|
+
*/
|
|
1770
2625
|
async send(envelope) {
|
|
1771
2626
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
|
|
1772
2627
|
throw new Error("WebSocket not connected");
|
|
1773
2628
|
}
|
|
2629
|
+
const messageId = envelope["message_id"];
|
|
2630
|
+
let answer = null;
|
|
2631
|
+
let timer;
|
|
2632
|
+
if (typeof messageId === "string" && !this._waiting.has(messageId)) {
|
|
2633
|
+
answer = new Promise((resolve, reject) => {
|
|
2634
|
+
this._waiting.set(messageId, { resolve, reject });
|
|
2635
|
+
timer = setTimeout(() => {
|
|
2636
|
+
log.warn(
|
|
2637
|
+
`No answer from the relay for ${messageId} within ${this._ackMs / 1e3}s`
|
|
2638
|
+
);
|
|
2639
|
+
this._settle(messageId, null);
|
|
2640
|
+
}, this._ackMs);
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
1774
2643
|
this._ws.send(JSON.stringify(envelope));
|
|
1775
2644
|
this._unanswered += 1;
|
|
2645
|
+
if (answer === null) return null;
|
|
2646
|
+
try {
|
|
2647
|
+
return await answer;
|
|
2648
|
+
} finally {
|
|
2649
|
+
clearTimeout(timer);
|
|
2650
|
+
}
|
|
1776
2651
|
}
|
|
1777
2652
|
async receive(limit = 50) {
|
|
1778
2653
|
const result = this._pending.slice(0, limit);
|
|
@@ -1799,6 +2674,21 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1799
2674
|
});
|
|
1800
2675
|
return this._settling;
|
|
1801
2676
|
}
|
|
2677
|
+
/** Hand the relay's answer (or a refusal) to the send waiting for it. */
|
|
2678
|
+
_settle(messageId, answer, error) {
|
|
2679
|
+
if (typeof messageId !== "string") return;
|
|
2680
|
+
const waiting = this._waiting.get(messageId);
|
|
2681
|
+
if (!waiting) return;
|
|
2682
|
+
this._waiting.delete(messageId);
|
|
2683
|
+
if (error) waiting.reject(error);
|
|
2684
|
+
else waiting.resolve(answer);
|
|
2685
|
+
}
|
|
2686
|
+
/** The socket went: no answers will come for what it carried. */
|
|
2687
|
+
_abandonWaiting() {
|
|
2688
|
+
const waiting = [...this._waiting.values()];
|
|
2689
|
+
this._waiting.clear();
|
|
2690
|
+
for (const w of waiting) w.resolve(null);
|
|
2691
|
+
}
|
|
1802
2692
|
_answered() {
|
|
1803
2693
|
this._unanswered = Math.max(0, this._unanswered - 1);
|
|
1804
2694
|
if (this._unanswered === 0) this._settled?.();
|
|
@@ -1830,6 +2720,7 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1830
2720
|
this._ws = null;
|
|
1831
2721
|
this._unanswered = 0;
|
|
1832
2722
|
this._settled?.();
|
|
2723
|
+
this._abandonWaiting();
|
|
1833
2724
|
if (!this._running) return;
|
|
1834
2725
|
attempt += 1;
|
|
1835
2726
|
const delay = Math.min(BASE_DELAY * Math.pow(2, attempt), MAX_DELAY);
|
|
@@ -1859,16 +2750,28 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1859
2750
|
}
|
|
1860
2751
|
} else if (msgType === "ack") {
|
|
1861
2752
|
this._answered();
|
|
2753
|
+
this._settle(msg["message_id"], msg);
|
|
2754
|
+
} else if (msgType === "relay.notice") {
|
|
2755
|
+
log.warn(
|
|
2756
|
+
`Relay notice: message ${String(msg["message_id"])} is ${String(msg["status"])}`
|
|
2757
|
+
);
|
|
1862
2758
|
} else if (msgType === "error" || "error" in msg) {
|
|
1863
2759
|
this._answered();
|
|
2760
|
+
const code = String(msg["error"] ?? msg["code"] ?? "error");
|
|
2761
|
+
const detail = typeof msg["detail"] === "string" ? msg["detail"] : void 0;
|
|
2762
|
+
log.warn(`Relay error: [${code}] ${detail ?? ""}`.trimEnd());
|
|
2763
|
+
const messageId = msg["message_id"];
|
|
2764
|
+
if (typeof messageId === "string") {
|
|
2765
|
+
this._settle(messageId, null, new SendRefusedError(messageId, code, detail));
|
|
2766
|
+
}
|
|
1864
2767
|
}
|
|
1865
2768
|
}
|
|
1866
2769
|
};
|
|
1867
2770
|
|
|
1868
2771
|
// src/sdk/transport/index.ts
|
|
1869
|
-
function createTransport(config, token,
|
|
2772
|
+
function createTransport(config, token, address2, onMessage) {
|
|
1870
2773
|
if (config.transportType === "http") {
|
|
1871
|
-
return new HTTPTransport(config.relayUrl, token,
|
|
2774
|
+
return new HTTPTransport(config.relayUrl, token, address2);
|
|
1872
2775
|
} else {
|
|
1873
2776
|
return new WebSocketTransport(config.relayWsUrl, token, onMessage);
|
|
1874
2777
|
}
|
|
@@ -2261,7 +3164,12 @@ var Agent = class {
|
|
|
2261
3164
|
}
|
|
2262
3165
|
// -- Messaging -----------------------------------------------------------
|
|
2263
3166
|
/**
|
|
2264
|
-
* Send an encrypted, signed message.
|
|
3167
|
+
* Send an encrypted, signed message.
|
|
3168
|
+
*
|
|
3169
|
+
* Returns the message id with what the relay did with it: `status`
|
|
3170
|
+
* "pending" means the recipient only takes approved contacts, and the
|
|
3171
|
+
* message was not kept (don't resend it). `delivered` is true only when
|
|
3172
|
+
* the relay says the recipient has it.
|
|
2265
3173
|
*/
|
|
2266
3174
|
async send(toAddress, message, options) {
|
|
2267
3175
|
await this._ensureConnected();
|
|
@@ -2293,14 +3201,20 @@ var Agent = class {
|
|
|
2293
3201
|
);
|
|
2294
3202
|
const wire = toWireDict(envelope);
|
|
2295
3203
|
const relayUrls = await this._getRelayUrls(toAddress);
|
|
3204
|
+
let answer;
|
|
2296
3205
|
if (relayUrls !== null && relayUrls.length === 1 && relayUrls[0] === this._config.relayUrl) {
|
|
2297
|
-
await this._transport.send(wire);
|
|
3206
|
+
answer = await this._transport.send(wire);
|
|
2298
3207
|
} else if (relayUrls !== null && relayUrls.length > 0) {
|
|
2299
|
-
await this._trySendWithFailover(wire, relayUrls);
|
|
3208
|
+
answer = await this._trySendWithFailover(wire, relayUrls);
|
|
2300
3209
|
} else {
|
|
2301
|
-
await this._transport.send(wire);
|
|
3210
|
+
answer = await this._transport.send(wire);
|
|
2302
3211
|
}
|
|
2303
|
-
|
|
3212
|
+
const status = sendStatus(answer);
|
|
3213
|
+
return {
|
|
3214
|
+
messageId: envelope.messageId,
|
|
3215
|
+
status,
|
|
3216
|
+
delivered: status === "delivered"
|
|
3217
|
+
};
|
|
2304
3218
|
}
|
|
2305
3219
|
/**
|
|
2306
3220
|
* Retrieve, decrypt, and verify pending messages.
|
|
@@ -2345,43 +3259,43 @@ var Agent = class {
|
|
|
2345
3259
|
* Adds the sender to contacts with trust_source='explicit-approval'
|
|
2346
3260
|
* and sends handshake.accept back.
|
|
2347
3261
|
*/
|
|
2348
|
-
async approve(
|
|
3262
|
+
async approve(address2) {
|
|
2349
3263
|
await this._ensureConnected();
|
|
2350
3264
|
const pendingList = this._contactBook.getPending();
|
|
2351
|
-
const entry = pendingList.find((p) => p.address ===
|
|
3265
|
+
const entry = pendingList.find((p) => p.address === address2);
|
|
2352
3266
|
if (!entry) {
|
|
2353
|
-
throw new UAMError(`No pending handshake from ${
|
|
3267
|
+
throw new UAMError(`No pending handshake from ${address2}`);
|
|
2354
3268
|
}
|
|
2355
3269
|
const cardDict = JSON.parse(entry.contactCard);
|
|
2356
3270
|
const card = contactCardFromDict(cardDict);
|
|
2357
3271
|
verifyContactCard(card);
|
|
2358
3272
|
const senderVk = deserializeVerifyKey(card.publicKey);
|
|
2359
|
-
bindCardToEnvelope(card, { fromAddress:
|
|
3273
|
+
bindCardToEnvelope(card, { fromAddress: address2 }, senderVk);
|
|
2360
3274
|
this._contactBook.addContact(card.address, card.publicKey, {
|
|
2361
3275
|
displayName: card.displayName,
|
|
2362
3276
|
trustState: "trusted",
|
|
2363
3277
|
trustSource: "explicit-approval"
|
|
2364
3278
|
});
|
|
2365
|
-
this._contactBook.removePending(
|
|
2366
|
-
await this._handshake._sendAccept(this,
|
|
3279
|
+
this._contactBook.removePending(address2);
|
|
3280
|
+
await this._handshake._sendAccept(this, address2, senderVk);
|
|
2367
3281
|
}
|
|
2368
3282
|
/**
|
|
2369
3283
|
* Deny a pending handshake request.
|
|
2370
3284
|
*
|
|
2371
3285
|
* Removes the entry from pending and sends handshake.deny.
|
|
2372
3286
|
*/
|
|
2373
|
-
async deny(
|
|
3287
|
+
async deny(address2) {
|
|
2374
3288
|
await this._ensureConnected();
|
|
2375
3289
|
const pendingList = this._contactBook.getPending();
|
|
2376
|
-
const entry = pendingList.find((p) => p.address ===
|
|
3290
|
+
const entry = pendingList.find((p) => p.address === address2);
|
|
2377
3291
|
if (!entry) {
|
|
2378
|
-
throw new UAMError(`No pending handshake from ${
|
|
3292
|
+
throw new UAMError(`No pending handshake from ${address2}`);
|
|
2379
3293
|
}
|
|
2380
3294
|
const cardDict = JSON.parse(entry.contactCard);
|
|
2381
3295
|
const card = contactCardFromDict(cardDict);
|
|
2382
3296
|
const senderVk = deserializeVerifyKey(card.publicKey);
|
|
2383
|
-
this._contactBook.removePending(
|
|
2384
|
-
await this._handshake._sendDeny(this,
|
|
3297
|
+
this._contactBook.removePending(address2);
|
|
3298
|
+
await this._handshake._sendDeny(this, address2, senderVk);
|
|
2385
3299
|
}
|
|
2386
3300
|
/**
|
|
2387
3301
|
* Block an address or domain pattern (HAND-04).
|
|
@@ -2407,7 +3321,7 @@ var Agent = class {
|
|
|
2407
3321
|
*
|
|
2408
3322
|
* Returns `true` if verified, `false` on timeout.
|
|
2409
3323
|
*/
|
|
2410
|
-
async verifyDomain(
|
|
3324
|
+
async verifyDomain(domain2, options) {
|
|
2411
3325
|
const { generateTxtRecord: generateTxtRecord2 } = await Promise.resolve().then(() => (init_dns_verifier(), dns_verifier_exports));
|
|
2412
3326
|
await this._ensureConnected();
|
|
2413
3327
|
const timeout = options?.timeout ?? 3e5;
|
|
@@ -2428,7 +3342,7 @@ var Agent = class {
|
|
|
2428
3342
|
"Content-Type": "application/json",
|
|
2429
3343
|
Authorization: `Bearer ${this._token}`
|
|
2430
3344
|
},
|
|
2431
|
-
body: JSON.stringify({ domain }),
|
|
3345
|
+
body: JSON.stringify({ domain: domain2 }),
|
|
2432
3346
|
signal: AbortSignal.timeout(3e4)
|
|
2433
3347
|
}
|
|
2434
3348
|
);
|
|
@@ -2481,7 +3395,7 @@ var Agent = class {
|
|
|
2481
3395
|
if (!resp.ok) {
|
|
2482
3396
|
throw new Error(`HTTP ${resp.status}`);
|
|
2483
3397
|
}
|
|
2484
|
-
return;
|
|
3398
|
+
return await readAnswer(resp);
|
|
2485
3399
|
} catch (err) {
|
|
2486
3400
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2487
3401
|
}
|
|
@@ -2779,13 +3693,13 @@ var Agent = class {
|
|
|
2779
3693
|
};
|
|
2780
3694
|
let resp = await this._postRegistration(body);
|
|
2781
3695
|
if (resp.status === 409) {
|
|
2782
|
-
const
|
|
3696
|
+
const address3 = await this._addressOnRelay();
|
|
2783
3697
|
resp = await this._postRegistration({
|
|
2784
3698
|
...body,
|
|
2785
|
-
proof: this._registrationProof(
|
|
3699
|
+
proof: this._registrationProof(address3)
|
|
2786
3700
|
});
|
|
2787
3701
|
if (resp.status === 409 || resp.status === 403) {
|
|
2788
|
-
throw this._ownershipNotProven(
|
|
3702
|
+
throw this._ownershipNotProven(address3);
|
|
2789
3703
|
}
|
|
2790
3704
|
}
|
|
2791
3705
|
if (!resp.ok) {
|
|
@@ -2795,26 +3709,26 @@ var Agent = class {
|
|
|
2795
3709
|
);
|
|
2796
3710
|
}
|
|
2797
3711
|
const data = await resp.json();
|
|
2798
|
-
const
|
|
2799
|
-
this._useAddress(
|
|
3712
|
+
const address2 = data["address"];
|
|
3713
|
+
this._useAddress(address2);
|
|
2800
3714
|
this._token = data["token"];
|
|
2801
3715
|
const { name } = this._config;
|
|
2802
3716
|
const token = this._token;
|
|
2803
3717
|
this._tryToSave("token", () => this._keyManager.saveToken(name, token));
|
|
2804
|
-
this._saveAddress(
|
|
3718
|
+
this._saveAddress(address2);
|
|
2805
3719
|
}
|
|
2806
3720
|
/** This agent's address on the relay, for signing a proof. */
|
|
2807
3721
|
async _addressOnRelay() {
|
|
2808
3722
|
const { name, explicitRelayDomain, relayDomain, relayUrl } = this._config;
|
|
2809
3723
|
if (explicitRelayDomain !== null) return `${name}::${explicitRelayDomain}`;
|
|
2810
|
-
const
|
|
2811
|
-
if (
|
|
2812
|
-
if (!this._relayMayUse(
|
|
3724
|
+
const domain2 = await this._fetchRelayDomain();
|
|
3725
|
+
if (domain2 === null) return `${name}::${relayDomain}`;
|
|
3726
|
+
if (!this._relayMayUse(domain2)) {
|
|
2813
3727
|
throw new UAMError(
|
|
2814
|
-
`The relay at ${relayUrl} says its domain is ${
|
|
3728
|
+
`The relay at ${relayUrl} says its domain is ${domain2}. Pass the domain option as ${JSON.stringify(domain2)} if that is right.`
|
|
2815
3729
|
);
|
|
2816
3730
|
}
|
|
2817
|
-
return `${name}::${
|
|
3731
|
+
return `${name}::${domain2}`;
|
|
2818
3732
|
}
|
|
2819
3733
|
/**
|
|
2820
3734
|
* True if the relay's URL fits `domain`, so a proof can be signed for it.
|
|
@@ -2822,14 +3736,14 @@ var Agent = class {
|
|
|
2822
3736
|
* A proof names an address on one relay, so it is only signed for a domain
|
|
2823
3737
|
* this URL belongs to. A relay on this machine may use any domain.
|
|
2824
3738
|
*/
|
|
2825
|
-
_relayMayUse(
|
|
3739
|
+
_relayMayUse(domain2) {
|
|
2826
3740
|
let host;
|
|
2827
3741
|
try {
|
|
2828
3742
|
host = new URL(this._config.relayUrl).hostname.toLowerCase();
|
|
2829
3743
|
} catch {
|
|
2830
3744
|
return false;
|
|
2831
3745
|
}
|
|
2832
|
-
const wanted =
|
|
3746
|
+
const wanted = domain2.toLowerCase();
|
|
2833
3747
|
return host === wanted || host.endsWith(`.${wanted}`) || host === "localhost" || host === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
2834
3748
|
}
|
|
2835
3749
|
_postRegistration(body) {
|
|
@@ -2844,27 +3758,27 @@ var Agent = class {
|
|
|
2844
3758
|
*
|
|
2845
3759
|
* The signed string must stay byte-identical to the Python SDK's.
|
|
2846
3760
|
*/
|
|
2847
|
-
_registrationProof(
|
|
3761
|
+
_registrationProof(address2) {
|
|
2848
3762
|
const timestamp = utcTimestamp();
|
|
2849
3763
|
const signature = signMessage(
|
|
2850
3764
|
new TextEncoder().encode(`uam-register-v1
|
|
2851
|
-
${
|
|
3765
|
+
${address2}
|
|
2852
3766
|
${timestamp}`),
|
|
2853
3767
|
this._keyManager.signingKey
|
|
2854
3768
|
);
|
|
2855
3769
|
return { timestamp, signature };
|
|
2856
3770
|
}
|
|
2857
|
-
_ownershipNotProven(
|
|
3771
|
+
_ownershipNotProven(address2) {
|
|
2858
3772
|
return new UAMError(
|
|
2859
|
-
`Address already registered, and this key could not prove ownership: ` +
|
|
3773
|
+
`Address already registered, and this key could not prove ownership: ` + address2
|
|
2860
3774
|
);
|
|
2861
3775
|
}
|
|
2862
3776
|
/** Remember `address` for this relay, if the key folder allows it. */
|
|
2863
|
-
_saveAddress(
|
|
3777
|
+
_saveAddress(address2) {
|
|
2864
3778
|
const { name, relayUrl } = this._config;
|
|
2865
3779
|
this._tryToSave(
|
|
2866
3780
|
"address",
|
|
2867
|
-
() => this._keyManager.saveAddress(name,
|
|
3781
|
+
() => this._keyManager.saveAddress(name, address2, relayUrl)
|
|
2868
3782
|
);
|
|
2869
3783
|
}
|
|
2870
3784
|
/** Saving is best effort: a key folder we can't write doesn't stop us. */
|
|
@@ -2877,12 +3791,12 @@ ${timestamp}`),
|
|
|
2877
3791
|
}
|
|
2878
3792
|
}
|
|
2879
3793
|
/** Take `address` as ours; its domain is this relay's real domain. */
|
|
2880
|
-
_useAddress(
|
|
2881
|
-
this._address =
|
|
2882
|
-
const
|
|
2883
|
-
this._config.relayDomain =
|
|
3794
|
+
_useAddress(address2) {
|
|
3795
|
+
this._address = address2;
|
|
3796
|
+
const domain2 = parseAddress(address2).domain;
|
|
3797
|
+
this._config.relayDomain = domain2;
|
|
2884
3798
|
if (this._resolver instanceof SmartResolver) {
|
|
2885
|
-
this._resolver.relayDomain =
|
|
3799
|
+
this._resolver.relayDomain = domain2;
|
|
2886
3800
|
}
|
|
2887
3801
|
}
|
|
2888
3802
|
/**
|
|
@@ -2897,11 +3811,11 @@ ${timestamp}`),
|
|
|
2897
3811
|
if (explicitRelayDomain !== null) return `${name}::${explicitRelayDomain}`;
|
|
2898
3812
|
const saved = this._keyManager.loadAddress(name, this._config.relayUrl);
|
|
2899
3813
|
if (saved) return saved;
|
|
2900
|
-
const
|
|
2901
|
-
if (
|
|
2902
|
-
const
|
|
2903
|
-
this._saveAddress(
|
|
2904
|
-
return
|
|
3814
|
+
const domain2 = await this._fetchRelayDomain();
|
|
3815
|
+
if (domain2 === null) return `${name}::${this._config.relayDomain}`;
|
|
3816
|
+
const address2 = `${name}::${domain2}`;
|
|
3817
|
+
this._saveAddress(address2);
|
|
3818
|
+
return address2;
|
|
2905
3819
|
}
|
|
2906
3820
|
/** Ask the relay for its domain; null if it cannot say. */
|
|
2907
3821
|
async _fetchRelayDomain() {
|
|
@@ -2910,9 +3824,9 @@ ${timestamp}`),
|
|
|
2910
3824
|
const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
2911
3825
|
if (!resp.ok) return null;
|
|
2912
3826
|
const data = await resp.json();
|
|
2913
|
-
const
|
|
2914
|
-
if (typeof
|
|
2915
|
-
return parseAddress(`${this._config.name}::${
|
|
3827
|
+
const domain2 = data["relay_domain"];
|
|
3828
|
+
if (typeof domain2 !== "string" || domain2 === "") return null;
|
|
3829
|
+
return parseAddress(`${this._config.name}::${domain2}`).domain;
|
|
2916
3830
|
} catch {
|
|
2917
3831
|
return null;
|
|
2918
3832
|
}
|
|
@@ -2921,11 +3835,11 @@ ${timestamp}`),
|
|
|
2921
3835
|
|
|
2922
3836
|
// src/sdk/webhook-verify.ts
|
|
2923
3837
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
2924
|
-
function verifyWebhookSignature(payload, signatureHeader,
|
|
3838
|
+
function verifyWebhookSignature(payload, signatureHeader, secret2) {
|
|
2925
3839
|
if (!signatureHeader.startsWith("sha256=")) {
|
|
2926
3840
|
return false;
|
|
2927
3841
|
}
|
|
2928
|
-
const expected = createHmac("sha256",
|
|
3842
|
+
const expected = createHmac("sha256", secret2).update(payload).digest("hex");
|
|
2929
3843
|
const received = signatureHeader.slice("sha256=".length);
|
|
2930
3844
|
const expectedBuf = Buffer.from(expected, "utf-8");
|
|
2931
3845
|
const receivedBuf = Buffer.from(received, "utf-8");
|
|
@@ -2940,7 +3854,11 @@ init_dns_verifier();
|
|
|
2940
3854
|
export {
|
|
2941
3855
|
Agent,
|
|
2942
3856
|
ContactBook,
|
|
3857
|
+
PENDING_NOTE,
|
|
3858
|
+
SEND_STATUSES,
|
|
3859
|
+
pendingNote,
|
|
2943
3860
|
protocol_exports as protocol,
|
|
3861
|
+
supports,
|
|
2944
3862
|
verifyWebhookSignature
|
|
2945
3863
|
};
|
|
2946
3864
|
//# sourceMappingURL=index.js.map
|