@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.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)
|
|
@@ -116,7 +116,11 @@ var index_exports = {};
|
|
|
116
116
|
__export(index_exports, {
|
|
117
117
|
Agent: () => Agent,
|
|
118
118
|
ContactBook: () => ContactBook,
|
|
119
|
+
PENDING_NOTE: () => PENDING_NOTE,
|
|
120
|
+
SEND_STATUSES: () => SEND_STATUSES,
|
|
121
|
+
pendingNote: () => pendingNote,
|
|
119
122
|
protocol: () => protocol_exports,
|
|
123
|
+
supports: () => supports,
|
|
120
124
|
verifyWebhookSignature: () => verifyWebhookSignature
|
|
121
125
|
});
|
|
122
126
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -132,32 +136,43 @@ __export(protocol_exports, {
|
|
|
132
136
|
EnvelopeTooLargeError: () => EnvelopeTooLargeError,
|
|
133
137
|
InvalidAddressError: () => InvalidAddressError,
|
|
134
138
|
InvalidContactCardError: () => InvalidContactCardError,
|
|
139
|
+
InvalidDidKeyError: () => InvalidDidKeyError,
|
|
135
140
|
InvalidEnvelopeError: () => InvalidEnvelopeError,
|
|
136
141
|
KeyPinningError: () => KeyPinningError,
|
|
137
142
|
MAX_ENVELOPE_AGE: () => MAX_ENVELOPE_AGE,
|
|
138
143
|
MAX_ENVELOPE_SIZE: () => MAX_ENVELOPE_SIZE,
|
|
139
144
|
MessageType: () => MessageType,
|
|
145
|
+
SendRefusedError: () => SendRefusedError,
|
|
140
146
|
SignatureError: () => SignatureError,
|
|
141
147
|
SignatureVerificationError: () => SignatureVerificationError,
|
|
148
|
+
SignedObjectError: () => SignedObjectError,
|
|
142
149
|
UAMError: () => UAMError,
|
|
143
150
|
UAM_VERSION: () => UAM_VERSION,
|
|
151
|
+
WebAuthnError: () => WebAuthnError,
|
|
152
|
+
approvalChallenge: () => approvalChallenge,
|
|
144
153
|
b64Decode: () => b64Decode,
|
|
145
154
|
b64Encode: () => b64Encode,
|
|
155
|
+
canonical: () => canonical,
|
|
146
156
|
canonicalize: () => canonicalize,
|
|
147
157
|
contactCardFromDict: () => contactCardFromDict,
|
|
148
158
|
contactCardToDict: () => contactCardToDict,
|
|
149
159
|
createContactCard: () => createContactCard,
|
|
150
160
|
createEnvelope: () => createEnvelope,
|
|
161
|
+
credentialDid: () => credentialDid,
|
|
162
|
+
decodeDidKey: () => decodeDidKey,
|
|
151
163
|
decryptPayload: () => decryptPayload,
|
|
152
164
|
decryptPayloadAnonymous: () => decryptPayloadAnonymous,
|
|
153
165
|
deserializeSigningKey: () => deserializeSigningKey,
|
|
154
166
|
deserializeVerifyKey: () => deserializeVerifyKey,
|
|
167
|
+
encodeDidKey: () => encodeDidKey,
|
|
155
168
|
encryptPayload: () => encryptPayload,
|
|
156
169
|
encryptPayloadAnonymous: () => encryptPayloadAnonymous,
|
|
157
170
|
fromWireDict: () => fromWireDict,
|
|
158
171
|
generateKeypair: () => generateKeypair,
|
|
159
172
|
generateNonce: () => generateNonce,
|
|
173
|
+
objectHash: () => objectHash,
|
|
160
174
|
parseAddress: () => parseAddress,
|
|
175
|
+
parseBody: () => parseBody,
|
|
161
176
|
publicKeyFingerprint: () => publicKeyFingerprint,
|
|
162
177
|
serializeSigningKey: () => serializeSigningKey,
|
|
163
178
|
serializeVerifyKey: () => serializeVerifyKey,
|
|
@@ -166,8 +181,11 @@ __export(protocol_exports, {
|
|
|
166
181
|
toWireDict: () => toWireDict,
|
|
167
182
|
utcTimestamp: () => utcTimestamp,
|
|
168
183
|
validateEnvelopeSize: () => validateEnvelopeSize,
|
|
184
|
+
validateSignedObject: () => validate,
|
|
185
|
+
verifyAssertion: () => verifyAssertion,
|
|
169
186
|
verifyContactCard: () => verifyContactCard,
|
|
170
187
|
verifyEnvelope: () => verifyEnvelope,
|
|
188
|
+
verifyRegistration: () => verifyRegistration,
|
|
171
189
|
verifySignature: () => verifySignature
|
|
172
190
|
});
|
|
173
191
|
|
|
@@ -262,6 +280,18 @@ var KeyPinningError = class _KeyPinningError extends UAMError {
|
|
|
262
280
|
Object.setPrototypeOf(this, _KeyPinningError.prototype);
|
|
263
281
|
}
|
|
264
282
|
};
|
|
283
|
+
var SendRefusedError = class extends UAMError {
|
|
284
|
+
messageId;
|
|
285
|
+
code;
|
|
286
|
+
detail;
|
|
287
|
+
constructor(messageId, code, detail) {
|
|
288
|
+
super(`Relay refused message ${messageId}: [${code}] ${detail ?? ""}`.trimEnd());
|
|
289
|
+
this.name = "SendRefusedError";
|
|
290
|
+
this.messageId = messageId;
|
|
291
|
+
this.code = code;
|
|
292
|
+
this.detail = detail;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
265
295
|
|
|
266
296
|
// src/protocol/address.ts
|
|
267
297
|
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])?)$/;
|
|
@@ -388,11 +418,11 @@ function encryptPayload(plaintext, senderSigningKey, recipientVerifyKey) {
|
|
|
388
418
|
try {
|
|
389
419
|
const senderCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_sk_to_curve25519(senderSigningKey);
|
|
390
420
|
const recipientCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_pk_to_curve25519(recipientVerifyKey);
|
|
391
|
-
const
|
|
392
|
-
const encrypted = import_libsodium_wrappers.default.crypto_box_easy(plaintext,
|
|
393
|
-
const combined = new Uint8Array(
|
|
394
|
-
combined.set(
|
|
395
|
-
combined.set(encrypted,
|
|
421
|
+
const nonce2 = import_libsodium_wrappers.default.randombytes_buf(import_libsodium_wrappers.default.crypto_box_NONCEBYTES);
|
|
422
|
+
const encrypted = import_libsodium_wrappers.default.crypto_box_easy(plaintext, nonce2, recipientCurve, senderCurve);
|
|
423
|
+
const combined = new Uint8Array(nonce2.length + encrypted.length);
|
|
424
|
+
combined.set(nonce2, 0);
|
|
425
|
+
combined.set(encrypted, nonce2.length);
|
|
396
426
|
return b64Encode(combined);
|
|
397
427
|
} catch (err) {
|
|
398
428
|
if (err instanceof EncryptionError) throw err;
|
|
@@ -402,11 +432,11 @@ function encryptPayload(plaintext, senderSigningKey, recipientVerifyKey) {
|
|
|
402
432
|
function decryptPayload(ciphertextB64, recipientSigningKey, senderVerifyKey) {
|
|
403
433
|
try {
|
|
404
434
|
const combined = b64Decode(ciphertextB64);
|
|
405
|
-
const
|
|
435
|
+
const nonce2 = combined.slice(0, import_libsodium_wrappers.default.crypto_box_NONCEBYTES);
|
|
406
436
|
const ciphertext = combined.slice(import_libsodium_wrappers.default.crypto_box_NONCEBYTES);
|
|
407
437
|
const recipientCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_sk_to_curve25519(recipientSigningKey);
|
|
408
438
|
const senderCurve = import_libsodium_wrappers.default.crypto_sign_ed25519_pk_to_curve25519(senderVerifyKey);
|
|
409
|
-
return import_libsodium_wrappers.default.crypto_box_open_easy(ciphertext,
|
|
439
|
+
return import_libsodium_wrappers.default.crypto_box_open_easy(ciphertext, nonce2, senderCurve, recipientCurve);
|
|
410
440
|
} catch (err) {
|
|
411
441
|
if (err instanceof DecryptionError) throw err;
|
|
412
442
|
throw new DecryptionError(err instanceof Error ? err.message : String(err));
|
|
@@ -514,7 +544,7 @@ async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaint
|
|
|
514
544
|
parseAddress(fromAddress);
|
|
515
545
|
parseAddress(toAddress);
|
|
516
546
|
const messageId = (0, import_node_crypto2.randomUUID)();
|
|
517
|
-
const
|
|
547
|
+
const nonce2 = generateNonce();
|
|
518
548
|
const timestamp = utcTimestamp();
|
|
519
549
|
let encryptedPayload;
|
|
520
550
|
if (typeValue === "handshake.request" /* HANDSHAKE_REQUEST */) {
|
|
@@ -533,7 +563,7 @@ async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaint
|
|
|
533
563
|
toAddress,
|
|
534
564
|
timestamp,
|
|
535
565
|
type: typeValue,
|
|
536
|
-
nonce,
|
|
566
|
+
nonce: nonce2,
|
|
537
567
|
payload: encryptedPayload,
|
|
538
568
|
signature: "",
|
|
539
569
|
threadId: options?.threadId,
|
|
@@ -552,7 +582,7 @@ async function createEnvelope(fromAddress, toAddress, messageType, payloadPlaint
|
|
|
552
582
|
toAddress,
|
|
553
583
|
timestamp,
|
|
554
584
|
type: typeValue,
|
|
555
|
-
nonce,
|
|
585
|
+
nonce: nonce2,
|
|
556
586
|
payload: encryptedPayload,
|
|
557
587
|
signature,
|
|
558
588
|
threadId: options?.threadId,
|
|
@@ -644,8 +674,8 @@ function contactCardFromDict(d, options) {
|
|
|
644
674
|
}
|
|
645
675
|
return card;
|
|
646
676
|
}
|
|
647
|
-
async function createContactCard(
|
|
648
|
-
parseAddress(
|
|
677
|
+
async function createContactCard(address2, displayName, relay, signingKey, options) {
|
|
678
|
+
parseAddress(address2);
|
|
649
679
|
const verifyKey = signingKey.slice(32);
|
|
650
680
|
const pubKeyB64 = serializeVerifyKey(verifyKey);
|
|
651
681
|
const fp = publicKeyFingerprint(verifyKey);
|
|
@@ -655,7 +685,7 @@ async function createContactCard(address, displayName, relay, signingKey, option
|
|
|
655
685
|
];
|
|
656
686
|
const tempCard = {
|
|
657
687
|
version: UAM_VERSION,
|
|
658
|
-
address,
|
|
688
|
+
address: address2,
|
|
659
689
|
displayName,
|
|
660
690
|
description: options?.description,
|
|
661
691
|
system: options?.system,
|
|
@@ -672,7 +702,7 @@ async function createContactCard(address, displayName, relay, signingKey, option
|
|
|
672
702
|
const signature = signMessage(canonicalize(signable), signingKey);
|
|
673
703
|
return Object.freeze({
|
|
674
704
|
version: UAM_VERSION,
|
|
675
|
-
address,
|
|
705
|
+
address: address2,
|
|
676
706
|
displayName,
|
|
677
707
|
description: options?.description,
|
|
678
708
|
system: options?.system,
|
|
@@ -699,8 +729,778 @@ function verifyContactCard(card) {
|
|
|
699
729
|
verifySignature(canonicalize(signable), card.signature, vk);
|
|
700
730
|
}
|
|
701
731
|
|
|
732
|
+
// src/protocol/signed.ts
|
|
733
|
+
var import_node_crypto3 = require("crypto");
|
|
734
|
+
|
|
735
|
+
// src/protocol/didkey.ts
|
|
736
|
+
var B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
737
|
+
var PREFIX = {
|
|
738
|
+
ed25519: new Uint8Array([237, 1]),
|
|
739
|
+
p256: new Uint8Array([128, 36])
|
|
740
|
+
};
|
|
741
|
+
var LENGTH = { ed25519: 32, p256: 33 };
|
|
742
|
+
var InvalidDidKeyError = class extends UAMError {
|
|
743
|
+
constructor(message) {
|
|
744
|
+
super(message);
|
|
745
|
+
this.name = "InvalidDidKeyError";
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
function b58encode(data) {
|
|
749
|
+
let n = 0n;
|
|
750
|
+
for (const byte of data) n = n << 8n | BigInt(byte);
|
|
751
|
+
let out = "";
|
|
752
|
+
while (n > 0n) {
|
|
753
|
+
out = B58[Number(n % 58n)] + out;
|
|
754
|
+
n /= 58n;
|
|
755
|
+
}
|
|
756
|
+
let zeros = 0;
|
|
757
|
+
while (zeros < data.length && data[zeros] === 0) zeros += 1;
|
|
758
|
+
return "1".repeat(zeros) + out;
|
|
759
|
+
}
|
|
760
|
+
function b58decode(text) {
|
|
761
|
+
let n = 0n;
|
|
762
|
+
for (const c of text) {
|
|
763
|
+
const i = B58.indexOf(c);
|
|
764
|
+
if (i < 0) throw new InvalidDidKeyError(`not base58btc: ${JSON.stringify(c)}`);
|
|
765
|
+
n = n * 58n + BigInt(i);
|
|
766
|
+
}
|
|
767
|
+
const body = [];
|
|
768
|
+
while (n > 0n) {
|
|
769
|
+
body.unshift(Number(n & 0xffn));
|
|
770
|
+
n >>= 8n;
|
|
771
|
+
}
|
|
772
|
+
let zeros = 0;
|
|
773
|
+
while (zeros < text.length && text[zeros] === "1") zeros += 1;
|
|
774
|
+
return new Uint8Array([...new Array(zeros).fill(0), ...body]);
|
|
775
|
+
}
|
|
776
|
+
function checkKey(kind, key) {
|
|
777
|
+
if (key.length !== LENGTH[kind]) {
|
|
778
|
+
throw new InvalidDidKeyError(`${kind} key must be ${LENGTH[kind]} bytes`);
|
|
779
|
+
}
|
|
780
|
+
if (kind === "p256" && key[0] !== 2 && key[0] !== 3) {
|
|
781
|
+
throw new InvalidDidKeyError("P-256 key must be a compressed point");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function encodeDidKey(kind, publicKey) {
|
|
785
|
+
if (!(kind in PREFIX)) throw new InvalidDidKeyError(`unsupported key type: ${kind}`);
|
|
786
|
+
checkKey(kind, publicKey);
|
|
787
|
+
return "did:key:z" + b58encode(new Uint8Array([...PREFIX[kind], ...publicKey]));
|
|
788
|
+
}
|
|
789
|
+
function decodeDidKey(did2) {
|
|
790
|
+
if (typeof did2 !== "string" || !did2.startsWith("did:key:z")) {
|
|
791
|
+
throw new InvalidDidKeyError("not a did:key with base58btc multibase");
|
|
792
|
+
}
|
|
793
|
+
const raw = b58decode(did2.slice("did:key:z".length));
|
|
794
|
+
for (const kind of Object.keys(PREFIX)) {
|
|
795
|
+
const prefix = PREFIX[kind];
|
|
796
|
+
if (raw.length >= prefix.length && prefix.every((b, i) => raw[i] === b)) {
|
|
797
|
+
const key = raw.slice(prefix.length);
|
|
798
|
+
checkKey(kind, key);
|
|
799
|
+
return { kind, key };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
throw new InvalidDidKeyError("unsupported key type");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/protocol/signed.ts
|
|
806
|
+
var MAX_INT = 2 ** 53 - 1;
|
|
807
|
+
var GRANT_MAX_DAYS = 90;
|
|
808
|
+
var INVITE_MAX_DAYS = 7;
|
|
809
|
+
var SignedObjectError = class extends UAMError {
|
|
810
|
+
constructor(message) {
|
|
811
|
+
super(message);
|
|
812
|
+
this.name = "SignedObjectError";
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
var has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
816
|
+
var Parser = class {
|
|
817
|
+
constructor(s) {
|
|
818
|
+
this.s = s;
|
|
819
|
+
}
|
|
820
|
+
i = 0;
|
|
821
|
+
fail(why) {
|
|
822
|
+
throw new SignedObjectError(`not JSON or outside the profile at ${this.i}: ${why}`);
|
|
823
|
+
}
|
|
824
|
+
ws() {
|
|
825
|
+
while (this.i < this.s.length && " \n\r".includes(this.s[this.i])) this.i += 1;
|
|
826
|
+
}
|
|
827
|
+
document() {
|
|
828
|
+
this.ws();
|
|
829
|
+
if (this.s[this.i] !== "{") this.fail("a body must be a JSON object");
|
|
830
|
+
const value = this.value();
|
|
831
|
+
this.ws();
|
|
832
|
+
if (this.i !== this.s.length) this.fail("trailing characters");
|
|
833
|
+
return value;
|
|
834
|
+
}
|
|
835
|
+
value() {
|
|
836
|
+
this.ws();
|
|
837
|
+
const c = this.s[this.i];
|
|
838
|
+
if (c === "{") return this.object();
|
|
839
|
+
if (c === "[") return this.array();
|
|
840
|
+
if (c === '"') return this.string();
|
|
841
|
+
if (c === "-" || c !== void 0 && c >= "0" && c <= "9") return this.number();
|
|
842
|
+
for (const [word, v] of [["true", true], ["false", false]]) {
|
|
843
|
+
if (this.s.startsWith(word, this.i)) {
|
|
844
|
+
this.i += word.length;
|
|
845
|
+
return v;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (this.s.startsWith("null", this.i)) this.fail("null is not allowed");
|
|
849
|
+
return this.fail("unexpected character");
|
|
850
|
+
}
|
|
851
|
+
object() {
|
|
852
|
+
this.i += 1;
|
|
853
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
854
|
+
this.ws();
|
|
855
|
+
if (this.s[this.i] === "}") {
|
|
856
|
+
this.i += 1;
|
|
857
|
+
return out;
|
|
858
|
+
}
|
|
859
|
+
for (; ; ) {
|
|
860
|
+
this.ws();
|
|
861
|
+
if (this.s[this.i] !== '"') this.fail("expected a key");
|
|
862
|
+
const key = this.string();
|
|
863
|
+
if (has(out, key)) this.fail(`duplicate key: ${JSON.stringify(key)}`);
|
|
864
|
+
this.ws();
|
|
865
|
+
if (this.s[this.i] !== ":") this.fail("expected ':'");
|
|
866
|
+
this.i += 1;
|
|
867
|
+
Object.defineProperty(out, key, {
|
|
868
|
+
value: this.value(),
|
|
869
|
+
enumerable: true,
|
|
870
|
+
writable: true,
|
|
871
|
+
configurable: true
|
|
872
|
+
});
|
|
873
|
+
this.ws();
|
|
874
|
+
if (this.s[this.i] === ",") {
|
|
875
|
+
this.i += 1;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (this.s[this.i] === "}") {
|
|
879
|
+
this.i += 1;
|
|
880
|
+
return out;
|
|
881
|
+
}
|
|
882
|
+
this.fail("expected ',' or '}'");
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
array() {
|
|
886
|
+
this.i += 1;
|
|
887
|
+
const out = [];
|
|
888
|
+
this.ws();
|
|
889
|
+
if (this.s[this.i] === "]") {
|
|
890
|
+
this.i += 1;
|
|
891
|
+
return out;
|
|
892
|
+
}
|
|
893
|
+
for (; ; ) {
|
|
894
|
+
out.push(this.value());
|
|
895
|
+
this.ws();
|
|
896
|
+
if (this.s[this.i] === ",") {
|
|
897
|
+
this.i += 1;
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (this.s[this.i] === "]") {
|
|
901
|
+
this.i += 1;
|
|
902
|
+
return out;
|
|
903
|
+
}
|
|
904
|
+
this.fail("expected ',' or ']'");
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
string() {
|
|
908
|
+
this.i += 1;
|
|
909
|
+
let out = "";
|
|
910
|
+
for (; ; ) {
|
|
911
|
+
const c = this.s[this.i];
|
|
912
|
+
if (c === void 0) this.fail("unterminated string");
|
|
913
|
+
this.i += 1;
|
|
914
|
+
if (c === '"') break;
|
|
915
|
+
if (c === "\\") {
|
|
916
|
+
const e = this.s[this.i];
|
|
917
|
+
this.i += 1;
|
|
918
|
+
const simple = {
|
|
919
|
+
'"': '"',
|
|
920
|
+
"\\": "\\",
|
|
921
|
+
"/": "/",
|
|
922
|
+
b: "\b",
|
|
923
|
+
f: "\f",
|
|
924
|
+
n: "\n",
|
|
925
|
+
r: "\r",
|
|
926
|
+
t: " "
|
|
927
|
+
};
|
|
928
|
+
if (e !== void 0 && has(simple, e)) {
|
|
929
|
+
out += simple[e];
|
|
930
|
+
} else if (e === "u") {
|
|
931
|
+
const hex = this.s.slice(this.i, this.i + 4);
|
|
932
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) this.fail("bad \\u escape");
|
|
933
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
934
|
+
this.i += 4;
|
|
935
|
+
} else {
|
|
936
|
+
this.fail("bad escape");
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
if (c < " ") this.fail("control character in string");
|
|
940
|
+
out += c;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
checkString(out);
|
|
944
|
+
return out;
|
|
945
|
+
}
|
|
946
|
+
number() {
|
|
947
|
+
const m = /^-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/.exec(this.s.slice(this.i));
|
|
948
|
+
if (!m) return this.fail("bad number");
|
|
949
|
+
const text = m[0];
|
|
950
|
+
if (m[2] !== void 0 || m[3] !== void 0) this.fail(`floats are not allowed: ${text}`);
|
|
951
|
+
if (text === "-0") this.fail("-0 is not allowed");
|
|
952
|
+
const big = BigInt(text);
|
|
953
|
+
if (big > BigInt(MAX_INT) || big < -BigInt(MAX_INT)) this.fail(`integer out of range: ${text}`);
|
|
954
|
+
this.i += text.length;
|
|
955
|
+
return Number(big);
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
function parseBody(text) {
|
|
959
|
+
let s;
|
|
960
|
+
if (typeof text === "string") {
|
|
961
|
+
s = text;
|
|
962
|
+
} else {
|
|
963
|
+
try {
|
|
964
|
+
s = new TextDecoder("utf-8", { fatal: true }).decode(text);
|
|
965
|
+
} catch {
|
|
966
|
+
throw new SignedObjectError("body is not UTF-8");
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return new Parser(s).document();
|
|
970
|
+
}
|
|
971
|
+
function checkString(value) {
|
|
972
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
973
|
+
const code = value.charCodeAt(i);
|
|
974
|
+
if (code < 32 || code > 126) {
|
|
975
|
+
throw new SignedObjectError("strings must be printable ASCII");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function isPlainObject(value) {
|
|
980
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
981
|
+
const proto = Object.getPrototypeOf(value);
|
|
982
|
+
return proto === null || proto === Object.prototype;
|
|
983
|
+
}
|
|
984
|
+
function encode(value) {
|
|
985
|
+
if (value === true) return "true";
|
|
986
|
+
if (value === false) return "false";
|
|
987
|
+
if (typeof value === "number") {
|
|
988
|
+
if (!Number.isSafeInteger(value) || Object.is(value, -0)) {
|
|
989
|
+
throw new SignedObjectError(`not an integer in range: ${value}`);
|
|
990
|
+
}
|
|
991
|
+
return String(value);
|
|
992
|
+
}
|
|
993
|
+
if (typeof value === "string") {
|
|
994
|
+
checkString(value);
|
|
995
|
+
return '"' + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
996
|
+
}
|
|
997
|
+
if (Array.isArray(value)) return "[" + value.map(encode).join(",") + "]";
|
|
998
|
+
if (isPlainObject(value)) {
|
|
999
|
+
return "{" + Object.keys(value).sort().map((key) => {
|
|
1000
|
+
checkString(key);
|
|
1001
|
+
return encode(key) + ":" + encode(value[key]);
|
|
1002
|
+
}).join(",") + "}";
|
|
1003
|
+
}
|
|
1004
|
+
if (value === null) throw new SignedObjectError("null is not allowed");
|
|
1005
|
+
throw new SignedObjectError(`not allowed: ${typeof value}`);
|
|
1006
|
+
}
|
|
1007
|
+
function canonical(body) {
|
|
1008
|
+
if (!isPlainObject(body)) throw new SignedObjectError("a body must be a JSON object");
|
|
1009
|
+
return new TextEncoder().encode(encode(body));
|
|
1010
|
+
}
|
|
1011
|
+
function hashCanonical(body) {
|
|
1012
|
+
return (0, import_node_crypto3.createHash)("sha256").update(`UAM-${String(body["type"])}-v1
|
|
1013
|
+
`).update(canonical(body)).digest("base64url");
|
|
1014
|
+
}
|
|
1015
|
+
function objectHash(body) {
|
|
1016
|
+
validate(body);
|
|
1017
|
+
return hashCanonical(body);
|
|
1018
|
+
}
|
|
1019
|
+
var TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/;
|
|
1020
|
+
var HASH_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
1021
|
+
var OWNER_ID_RE = /^[A-Za-z0-9_-]{22}$/;
|
|
1022
|
+
var NONCE_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
1023
|
+
var SECRET_RE = /^[A-Za-z0-9_-]{22}$/;
|
|
1024
|
+
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])?)+$/;
|
|
1025
|
+
function fail(path, why) {
|
|
1026
|
+
throw new SignedObjectError(`${path}: ${why}`);
|
|
1027
|
+
}
|
|
1028
|
+
var string_ = (v, p) => {
|
|
1029
|
+
if (typeof v !== "string" || v.length === 0) fail(p, "must be a non-empty string");
|
|
1030
|
+
};
|
|
1031
|
+
var label = (v, p) => {
|
|
1032
|
+
string_(v, p);
|
|
1033
|
+
if (v.length > 64) fail(p, "at most 64 characters");
|
|
1034
|
+
if (v.includes('"')) fail(p, 'must not contain "');
|
|
1035
|
+
};
|
|
1036
|
+
var matching = (re, what) => (v, p) => {
|
|
1037
|
+
if (typeof v !== "string" || !re.test(v)) fail(p, `must be ${what}`);
|
|
1038
|
+
};
|
|
1039
|
+
var hash = matching(HASH_RE, "a hash (43 base64url characters)");
|
|
1040
|
+
var ownerId = matching(OWNER_ID_RE, "an owner id (22 base64url characters)");
|
|
1041
|
+
var nonce = matching(NONCE_RE, "a nonce (16-64 base64url characters)");
|
|
1042
|
+
var secret = matching(SECRET_RE, "a secret (22 base64url characters)");
|
|
1043
|
+
var domain = matching(DOMAIN_RE, "a lowercase domain");
|
|
1044
|
+
function parseTime(value) {
|
|
1045
|
+
const m = TIME_RE.exec(value);
|
|
1046
|
+
if (!m) return null;
|
|
1047
|
+
const [y, mo, d, h, mi, s] = m.slice(1).map(Number);
|
|
1048
|
+
const t = Date.UTC(y, mo - 1, d, h, mi, s);
|
|
1049
|
+
const back = new Date(t);
|
|
1050
|
+
if (back.getUTCFullYear() !== y || back.getUTCMonth() !== mo - 1 || back.getUTCDate() !== d || back.getUTCHours() !== h || back.getUTCMinutes() !== mi || back.getUTCSeconds() !== s) {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
return t;
|
|
1054
|
+
}
|
|
1055
|
+
var time = (v, p) => {
|
|
1056
|
+
if (typeof v !== "string" || !TIME_RE.test(v)) fail(p, "must be YYYY-MM-DDTHH:MM:SSZ");
|
|
1057
|
+
if (parseTime(v) === null) fail(p, "not a real time");
|
|
1058
|
+
};
|
|
1059
|
+
var bool = (v, p) => {
|
|
1060
|
+
if (typeof v !== "boolean") fail(p, "must be true or false");
|
|
1061
|
+
};
|
|
1062
|
+
var constant = (expected) => (v, p) => {
|
|
1063
|
+
if (v !== expected) fail(p, `must be ${JSON.stringify(expected)}`);
|
|
1064
|
+
};
|
|
1065
|
+
var oneOf = (...options) => (v, p) => {
|
|
1066
|
+
if (typeof v !== "string" || !options.includes(v)) fail(p, `must be one of ${options.join(", ")}`);
|
|
1067
|
+
};
|
|
1068
|
+
var uint = (v, p) => {
|
|
1069
|
+
if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 0) {
|
|
1070
|
+
fail(p, "must be a non-negative integer");
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
var address = (v, p) => {
|
|
1074
|
+
if (typeof v !== "string") fail(p, "must be an address");
|
|
1075
|
+
let full;
|
|
1076
|
+
try {
|
|
1077
|
+
full = parseAddress(v).full;
|
|
1078
|
+
} catch {
|
|
1079
|
+
return fail(p, "must be an address");
|
|
1080
|
+
}
|
|
1081
|
+
if (full !== v) fail(p, "must be an address in canonical (lowercase) form");
|
|
1082
|
+
};
|
|
1083
|
+
var did = (kind) => (v, p) => {
|
|
1084
|
+
let found;
|
|
1085
|
+
try {
|
|
1086
|
+
found = decodeDidKey(v).kind;
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
return fail(p, err instanceof Error ? err.message : String(err));
|
|
1089
|
+
}
|
|
1090
|
+
if (found !== kind) fail(p, `must be a ${kind} did:key`);
|
|
1091
|
+
};
|
|
1092
|
+
var list = (item, minLen = 1, maxLen = 64) => (v, p) => {
|
|
1093
|
+
if (!Array.isArray(v) || v.length < minLen || v.length > maxLen) {
|
|
1094
|
+
fail(p, `must be a list of ${minLen}-${maxLen} items`);
|
|
1095
|
+
}
|
|
1096
|
+
v.forEach((entry, i) => item(entry, `${p}[${i}]`));
|
|
1097
|
+
};
|
|
1098
|
+
var object = (required, optional = {}) => (v, p) => {
|
|
1099
|
+
if (!isPlainObject(v)) fail(p, "must be an object");
|
|
1100
|
+
for (const key of Object.keys(v)) {
|
|
1101
|
+
if (!has(required, key) && !has(optional, key)) fail(`${p}.${key}`, "unknown member");
|
|
1102
|
+
}
|
|
1103
|
+
for (const [key, member] of Object.entries(required)) {
|
|
1104
|
+
if (!has(v, key)) fail(`${p}.${key}`, "missing");
|
|
1105
|
+
member(v[key], `${p}.${key}`);
|
|
1106
|
+
}
|
|
1107
|
+
for (const [key, member] of Object.entries(optional)) {
|
|
1108
|
+
if (has(v, key)) member(v[key], `${p}.${key}`);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
var location = (v, p) => {
|
|
1112
|
+
if (v !== "shared") address(v, p);
|
|
1113
|
+
};
|
|
1114
|
+
var access = object({ actions: list(oneOf("message"), 1, 1), locations: list(location) });
|
|
1115
|
+
var ownerRef = object({ relay: domain, ownerId, keysHash: hash });
|
|
1116
|
+
var ownerRefNoKeys = object({ relay: domain, ownerId });
|
|
1117
|
+
var passkey = did("p256");
|
|
1118
|
+
var agentKey = did("ed25519");
|
|
1119
|
+
var keysOwner = object({ relay: domain }, { ownerId });
|
|
1120
|
+
var EXT = { ext: () => void 0, crit: () => void 0 };
|
|
1121
|
+
function checkExt(body) {
|
|
1122
|
+
if (has(body, "ext") && !isPlainObject(body["ext"])) fail("ext", "must be an object");
|
|
1123
|
+
if (has(body, "crit")) {
|
|
1124
|
+
const crit = body["crit"];
|
|
1125
|
+
if (!has(body, "ext")) fail("crit", "needs ext");
|
|
1126
|
+
if (!Array.isArray(crit) || !crit.every((k) => typeof k === "string")) {
|
|
1127
|
+
fail("crit", "must be a list of ext keys");
|
|
1128
|
+
}
|
|
1129
|
+
for (const key of crit) {
|
|
1130
|
+
if (!has(body["ext"], key)) fail(`crit.${key}`, "not in ext");
|
|
1131
|
+
fail(`crit.${key}`, "unknown critical extension");
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function span(body, from, to) {
|
|
1136
|
+
return parseTime(body[to]) - parseTime(body[from]);
|
|
1137
|
+
}
|
|
1138
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
1139
|
+
var SCHEMAS = {
|
|
1140
|
+
UamOwnerKeys(body) {
|
|
1141
|
+
object(
|
|
1142
|
+
{
|
|
1143
|
+
type: constant("UamOwnerKeys"),
|
|
1144
|
+
v: constant(1),
|
|
1145
|
+
owner: keysOwner,
|
|
1146
|
+
seq: uint,
|
|
1147
|
+
keys: list(object({ id: passkey, rpId: domain, backupEligible: bool, label }), 1, 16)
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
prev: hash,
|
|
1151
|
+
removed: list(object({ id: passkey, reason: oneOf("removed", "compromised") }), 1, 64),
|
|
1152
|
+
recovery: bool,
|
|
1153
|
+
...EXT
|
|
1154
|
+
}
|
|
1155
|
+
)(body, "$");
|
|
1156
|
+
const owner = body["owner"];
|
|
1157
|
+
if (body["seq"] === 0) {
|
|
1158
|
+
if (has(owner, "ownerId") || has(body, "prev")) fail("$", "entry 0 has no owner.ownerId and no prev");
|
|
1159
|
+
} else if (!has(owner, "ownerId") || !has(body, "prev")) {
|
|
1160
|
+
fail("$", "a later entry needs owner.ownerId and prev");
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
UamOwnerLink(body) {
|
|
1164
|
+
object({
|
|
1165
|
+
type: constant("UamOwnerLink"),
|
|
1166
|
+
v: constant(1),
|
|
1167
|
+
owner: ownerRef,
|
|
1168
|
+
agent: address,
|
|
1169
|
+
agentKey,
|
|
1170
|
+
relay: domain,
|
|
1171
|
+
via: oneOf("setup", "link"),
|
|
1172
|
+
shared: bool,
|
|
1173
|
+
effectiveAt: time,
|
|
1174
|
+
validFrom: time,
|
|
1175
|
+
nonce
|
|
1176
|
+
}, { ...EXT })(body, "$");
|
|
1177
|
+
},
|
|
1178
|
+
UamOwnerUnlink(body) {
|
|
1179
|
+
object({
|
|
1180
|
+
type: constant("UamOwnerUnlink"),
|
|
1181
|
+
v: constant(1),
|
|
1182
|
+
owner: ownerRef,
|
|
1183
|
+
agent: address,
|
|
1184
|
+
agentKey,
|
|
1185
|
+
reason: oneOf("owner", "admin", "relink", "authority", "lapsed"),
|
|
1186
|
+
validFrom: time,
|
|
1187
|
+
nonce
|
|
1188
|
+
}, { ...EXT })(body, "$");
|
|
1189
|
+
},
|
|
1190
|
+
UamInvite(body) {
|
|
1191
|
+
object(
|
|
1192
|
+
{
|
|
1193
|
+
type: constant("UamInvite"),
|
|
1194
|
+
v: constant(1),
|
|
1195
|
+
inviter: ownerRef,
|
|
1196
|
+
offer: access,
|
|
1197
|
+
ask: access,
|
|
1198
|
+
secretHash: hash,
|
|
1199
|
+
mode: oneOf("preapproved", "confirm"),
|
|
1200
|
+
maxUses: constant(1),
|
|
1201
|
+
validFrom: time,
|
|
1202
|
+
redeemBy: time,
|
|
1203
|
+
nonce
|
|
1204
|
+
},
|
|
1205
|
+
{ emailHash: hash, ...EXT }
|
|
1206
|
+
)(body, "$");
|
|
1207
|
+
if (body["mode"] === "preapproved" && !has(body, "emailHash")) {
|
|
1208
|
+
fail("$.emailHash", "a pre-approved invite names the invitee's email");
|
|
1209
|
+
}
|
|
1210
|
+
const d = span(body, "validFrom", "redeemBy");
|
|
1211
|
+
if (!(d > 0 && d <= INVITE_MAX_DAYS * DAY)) {
|
|
1212
|
+
fail("$.redeemBy", `must be after validFrom, within ${INVITE_MAX_DAYS} days`);
|
|
1213
|
+
}
|
|
1214
|
+
},
|
|
1215
|
+
UamRedemption(body) {
|
|
1216
|
+
object({
|
|
1217
|
+
type: constant("UamRedemption"),
|
|
1218
|
+
v: constant(1),
|
|
1219
|
+
invite: hash,
|
|
1220
|
+
secret,
|
|
1221
|
+
invitee: ownerRef,
|
|
1222
|
+
issuer: passkey,
|
|
1223
|
+
createdAt: time,
|
|
1224
|
+
nonce
|
|
1225
|
+
}, { ...EXT })(body, "$");
|
|
1226
|
+
},
|
|
1227
|
+
UamContactGrant(body) {
|
|
1228
|
+
object({
|
|
1229
|
+
type: constant("UamContactGrant"),
|
|
1230
|
+
v: constant(1),
|
|
1231
|
+
issuer: passkey,
|
|
1232
|
+
issuerOwner: ownerRefNoKeys,
|
|
1233
|
+
credentialSubject: subject,
|
|
1234
|
+
authorization_details: list(object({
|
|
1235
|
+
type: constant("uam_contact"),
|
|
1236
|
+
actions: list(oneOf("message"), 1, 1),
|
|
1237
|
+
locations: list(location)
|
|
1238
|
+
}), 1, 8),
|
|
1239
|
+
via: oneOf("intro_email", "intro_code", "intro_qr", "request", "carried_over"),
|
|
1240
|
+
enforcer: domain,
|
|
1241
|
+
validFrom: time,
|
|
1242
|
+
validUntil: time,
|
|
1243
|
+
nonce
|
|
1244
|
+
}, { ...EXT })(body, "$");
|
|
1245
|
+
const d = span(body, "validFrom", "validUntil");
|
|
1246
|
+
if (!(d > 0 && d <= GRANT_MAX_DAYS * DAY)) {
|
|
1247
|
+
fail("$.validUntil", `must be after validFrom, within ${GRANT_MAX_DAYS} days`);
|
|
1248
|
+
}
|
|
1249
|
+
},
|
|
1250
|
+
// Who may message an agent, as its owner approved it (P2e). Only a change
|
|
1251
|
+
// that widens it is signed; narrowing needs no passkey.
|
|
1252
|
+
UamAgentPolicy(body) {
|
|
1253
|
+
object({
|
|
1254
|
+
type: constant("UamAgentPolicy"),
|
|
1255
|
+
v: constant(1),
|
|
1256
|
+
owner: ownerRef,
|
|
1257
|
+
agent: address,
|
|
1258
|
+
agentKey,
|
|
1259
|
+
policy: oneOf("open", "contacts"),
|
|
1260
|
+
sameOwner: bool,
|
|
1261
|
+
validFrom: time,
|
|
1262
|
+
nonce
|
|
1263
|
+
}, { ...EXT })(body, "$");
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
var SUBJECTS = {
|
|
1267
|
+
owner: object({ kind: constant("owner"), owner: ownerRef }),
|
|
1268
|
+
invitee: object({ kind: constant("invitee"), invite: hash }),
|
|
1269
|
+
agent: object({ kind: constant("agent"), id: agentKey, address })
|
|
1270
|
+
};
|
|
1271
|
+
function subject(value, path) {
|
|
1272
|
+
const kind = isPlainObject(value) ? value["kind"] : void 0;
|
|
1273
|
+
if (typeof kind !== "string" || !has(SUBJECTS, kind)) {
|
|
1274
|
+
fail(`${path}.kind`, `must be one of ${Object.keys(SUBJECTS).join(", ")}`);
|
|
1275
|
+
}
|
|
1276
|
+
SUBJECTS[kind](value, path);
|
|
1277
|
+
}
|
|
1278
|
+
function validate(body) {
|
|
1279
|
+
if (!isPlainObject(body)) throw new SignedObjectError("a body must be a JSON object");
|
|
1280
|
+
encode(body);
|
|
1281
|
+
const type = body["type"];
|
|
1282
|
+
if (typeof type !== "string" || !has(SCHEMAS, type)) {
|
|
1283
|
+
throw new SignedObjectError(`unknown type: ${JSON.stringify(type)}`);
|
|
1284
|
+
}
|
|
1285
|
+
SCHEMAS[type](body);
|
|
1286
|
+
checkExt(body);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/protocol/webauthn.ts
|
|
1290
|
+
var import_node_crypto4 = require("crypto");
|
|
1291
|
+
var import_p256 = require("@noble/curves/p256");
|
|
1292
|
+
var ES256 = -7;
|
|
1293
|
+
var UP = 1;
|
|
1294
|
+
var UV = 4;
|
|
1295
|
+
var BE = 8;
|
|
1296
|
+
var AT = 64;
|
|
1297
|
+
var ED = 128;
|
|
1298
|
+
var WebAuthnError = class extends UAMError {
|
|
1299
|
+
constructor(message) {
|
|
1300
|
+
super(message);
|
|
1301
|
+
this.name = "WebAuthnError";
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
function credentialDid(credential) {
|
|
1305
|
+
return encodeDidKey("p256", credential.publicKey);
|
|
1306
|
+
}
|
|
1307
|
+
var sha256 = (...parts) => {
|
|
1308
|
+
const h = (0, import_node_crypto4.createHash)("sha256");
|
|
1309
|
+
for (const part of parts) h.update(part);
|
|
1310
|
+
return new Uint8Array(h.digest());
|
|
1311
|
+
};
|
|
1312
|
+
var equal = (a, b) => a.length === b.length && a.every((byte, i) => byte === b[i]);
|
|
1313
|
+
function cbor(data, start) {
|
|
1314
|
+
let i = start;
|
|
1315
|
+
if (i >= data.length) throw new WebAuthnError("CBOR: truncated");
|
|
1316
|
+
const first = data[i];
|
|
1317
|
+
const major = first >> 5;
|
|
1318
|
+
const info = first & 31;
|
|
1319
|
+
i += 1;
|
|
1320
|
+
if (major === 7) {
|
|
1321
|
+
if (info === 20) return [false, i];
|
|
1322
|
+
if (info === 21) return [true, i];
|
|
1323
|
+
throw new WebAuthnError("CBOR: unsupported simple value or float");
|
|
1324
|
+
}
|
|
1325
|
+
let n;
|
|
1326
|
+
if (info < 24) {
|
|
1327
|
+
n = info;
|
|
1328
|
+
} else if (info >= 24 && info <= 27) {
|
|
1329
|
+
const size = 1 << info - 24;
|
|
1330
|
+
if (i + size > data.length) throw new WebAuthnError("CBOR: truncated");
|
|
1331
|
+
let big = 0n;
|
|
1332
|
+
for (let k = 0; k < size; k += 1) big = big << 8n | BigInt(data[i + k]);
|
|
1333
|
+
if (big > BigInt(Number.MAX_SAFE_INTEGER)) throw new WebAuthnError("CBOR: number too large");
|
|
1334
|
+
n = Number(big);
|
|
1335
|
+
i += size;
|
|
1336
|
+
} else {
|
|
1337
|
+
throw new WebAuthnError("CBOR: indefinite lengths are not supported");
|
|
1338
|
+
}
|
|
1339
|
+
if (major === 0) return [n, i];
|
|
1340
|
+
if (major === 1) return [-1 - n, i];
|
|
1341
|
+
if (major === 2 || major === 3) {
|
|
1342
|
+
if (i + n > data.length) throw new WebAuthnError("CBOR: truncated");
|
|
1343
|
+
const raw = data.slice(i, i + n);
|
|
1344
|
+
i += n;
|
|
1345
|
+
if (major === 2) return [raw, i];
|
|
1346
|
+
try {
|
|
1347
|
+
return [new TextDecoder("utf-8", { fatal: true }).decode(raw), i];
|
|
1348
|
+
} catch {
|
|
1349
|
+
throw new WebAuthnError("CBOR: text is not UTF-8");
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (major === 4) {
|
|
1353
|
+
const items = [];
|
|
1354
|
+
for (let k = 0; k < n; k += 1) {
|
|
1355
|
+
const [item, next] = cbor(data, i);
|
|
1356
|
+
items.push(item);
|
|
1357
|
+
i = next;
|
|
1358
|
+
}
|
|
1359
|
+
return [items, i];
|
|
1360
|
+
}
|
|
1361
|
+
if (major === 5) {
|
|
1362
|
+
const out = /* @__PURE__ */ new Map();
|
|
1363
|
+
for (let k = 0; k < n; k += 1) {
|
|
1364
|
+
const [key, afterKey] = cbor(data, i);
|
|
1365
|
+
if (typeof key !== "number" && typeof key !== "string") {
|
|
1366
|
+
throw new WebAuthnError("CBOR: map keys must be integers or text");
|
|
1367
|
+
}
|
|
1368
|
+
if (out.has(key)) throw new WebAuthnError("CBOR: duplicate map key");
|
|
1369
|
+
const [value, afterValue] = cbor(data, afterKey);
|
|
1370
|
+
out.set(key, value);
|
|
1371
|
+
i = afterValue;
|
|
1372
|
+
}
|
|
1373
|
+
return [out, i];
|
|
1374
|
+
}
|
|
1375
|
+
throw new WebAuthnError("CBOR: tags are not supported");
|
|
1376
|
+
}
|
|
1377
|
+
function cborWhole(data) {
|
|
1378
|
+
const [value, end] = cbor(data, 0);
|
|
1379
|
+
if (end !== data.length) throw new WebAuthnError("CBOR: trailing bytes");
|
|
1380
|
+
return value;
|
|
1381
|
+
}
|
|
1382
|
+
function checkClientData(raw, kind, challenge, origin) {
|
|
1383
|
+
let data;
|
|
1384
|
+
try {
|
|
1385
|
+
data = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw));
|
|
1386
|
+
} catch {
|
|
1387
|
+
throw new WebAuthnError("clientDataJSON is not JSON");
|
|
1388
|
+
}
|
|
1389
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
1390
|
+
throw new WebAuthnError("clientDataJSON is not an object");
|
|
1391
|
+
}
|
|
1392
|
+
const d = data;
|
|
1393
|
+
if (d["type"] !== kind) throw new WebAuthnError(`clientData.type must be ${kind}`);
|
|
1394
|
+
if (d["challenge"] !== Buffer.from(challenge).toString("base64url")) {
|
|
1395
|
+
throw new WebAuthnError("clientData.challenge doesn't match");
|
|
1396
|
+
}
|
|
1397
|
+
if (d["origin"] !== origin) throw new WebAuthnError("clientData.origin doesn't match");
|
|
1398
|
+
if ("crossOrigin" in d && d["crossOrigin"] !== false) {
|
|
1399
|
+
throw new WebAuthnError("cross-origin ceremonies are refused");
|
|
1400
|
+
}
|
|
1401
|
+
if ("topOrigin" in d) throw new WebAuthnError("ceremonies in a frame are refused");
|
|
1402
|
+
}
|
|
1403
|
+
function checkAuthData(data, rpId) {
|
|
1404
|
+
if (data.length < 37) throw new WebAuthnError("authenticatorData too short");
|
|
1405
|
+
if (!equal(data.slice(0, 32), sha256(new TextEncoder().encode(rpId)))) {
|
|
1406
|
+
throw new WebAuthnError("rpIdHash doesn't match");
|
|
1407
|
+
}
|
|
1408
|
+
const flags = data[32];
|
|
1409
|
+
if (!(flags & UP)) throw new WebAuthnError("user not present");
|
|
1410
|
+
if (!(flags & UV)) throw new WebAuthnError("user not verified");
|
|
1411
|
+
const count = new DataView(data.buffer, data.byteOffset + 33, 4).getUint32(0, false);
|
|
1412
|
+
return { flags, count };
|
|
1413
|
+
}
|
|
1414
|
+
function point(bytes) {
|
|
1415
|
+
try {
|
|
1416
|
+
return import_p256.p256.ProjectivePoint.fromHex(bytes);
|
|
1417
|
+
} catch {
|
|
1418
|
+
throw new WebAuthnError("not a P-256 point");
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function verifyRegistration(args) {
|
|
1422
|
+
checkClientData(args.clientDataJSON, "webauthn.create", args.challenge, args.origin);
|
|
1423
|
+
const att = cborWhole(args.attestationObject);
|
|
1424
|
+
if (!(att instanceof Map) || att.size !== 3 || !att.has("fmt") || !att.has("attStmt") || !att.has("authData")) {
|
|
1425
|
+
throw new WebAuthnError("attestationObject must have fmt, attStmt and authData");
|
|
1426
|
+
}
|
|
1427
|
+
const stmt = att.get("attStmt");
|
|
1428
|
+
if (att.get("fmt") !== "none" || !(stmt instanceof Map) || stmt.size !== 0) {
|
|
1429
|
+
throw new WebAuthnError("only attestation format 'none' is accepted");
|
|
1430
|
+
}
|
|
1431
|
+
const data = att.get("authData");
|
|
1432
|
+
if (!(data instanceof Uint8Array)) throw new WebAuthnError("authData must be bytes");
|
|
1433
|
+
const { flags, count } = checkAuthData(data, args.rpId);
|
|
1434
|
+
if (!(flags & AT)) throw new WebAuthnError("no attested credential data");
|
|
1435
|
+
if (data.length < 55) throw new WebAuthnError("attested credential data too short");
|
|
1436
|
+
const idLen = data[53] << 8 | data[54];
|
|
1437
|
+
const credentialId = data.slice(55, 55 + idLen);
|
|
1438
|
+
if (credentialId.length !== idLen) throw new WebAuthnError("credential id truncated");
|
|
1439
|
+
const [cose, end] = cbor(data, 55 + idLen);
|
|
1440
|
+
if (!(flags & ED) && end !== data.length) {
|
|
1441
|
+
throw new WebAuthnError("trailing bytes after the credential key");
|
|
1442
|
+
}
|
|
1443
|
+
if (!(cose instanceof Map) || cose.get(1) !== 2 || cose.get(3) !== ES256 || cose.get(-1) !== 1) {
|
|
1444
|
+
throw new WebAuthnError("only ES256 (P-256) passkeys are accepted");
|
|
1445
|
+
}
|
|
1446
|
+
const x = cose.get(-2);
|
|
1447
|
+
const y = cose.get(-3);
|
|
1448
|
+
if (!(x instanceof Uint8Array) || !(y instanceof Uint8Array) || x.length !== 32 || y.length !== 32) {
|
|
1449
|
+
throw new WebAuthnError("bad P-256 coordinates");
|
|
1450
|
+
}
|
|
1451
|
+
const key = point(new Uint8Array([4, ...x, ...y]));
|
|
1452
|
+
return {
|
|
1453
|
+
id: credentialId,
|
|
1454
|
+
alg: ES256,
|
|
1455
|
+
publicKey: key.toRawBytes(true),
|
|
1456
|
+
backupEligible: Boolean(flags & BE),
|
|
1457
|
+
signCount: count
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
function verifyAssertion(credential, args) {
|
|
1461
|
+
if (credential.alg !== ES256) throw new WebAuthnError("only ES256 (P-256) passkeys are accepted");
|
|
1462
|
+
checkClientData(args.clientDataJSON, "webauthn.get", args.challenge, args.origin);
|
|
1463
|
+
const { flags, count } = checkAuthData(args.authenticatorData, args.rpId);
|
|
1464
|
+
if (Boolean(flags & BE) !== credential.backupEligible) {
|
|
1465
|
+
throw new WebAuthnError("backup eligibility changed since registration");
|
|
1466
|
+
}
|
|
1467
|
+
const key = point(credential.publicKey);
|
|
1468
|
+
const digest = sha256(args.authenticatorData, sha256(args.clientDataJSON));
|
|
1469
|
+
let ok = false;
|
|
1470
|
+
try {
|
|
1471
|
+
ok = import_p256.p256.verify(args.signature, digest, key.toRawBytes(false), {
|
|
1472
|
+
format: "der",
|
|
1473
|
+
lowS: false,
|
|
1474
|
+
prehash: false
|
|
1475
|
+
});
|
|
1476
|
+
} catch {
|
|
1477
|
+
ok = false;
|
|
1478
|
+
}
|
|
1479
|
+
if (!ok) throw new WebAuthnError("bad signature");
|
|
1480
|
+
const checkCounter = args.checkCounter ?? true;
|
|
1481
|
+
if (checkCounter && (count || credential.signCount) && count <= credential.signCount) {
|
|
1482
|
+
throw new WebAuthnError("sign count didn't go up: the passkey may have been cloned");
|
|
1483
|
+
}
|
|
1484
|
+
return count;
|
|
1485
|
+
}
|
|
1486
|
+
function approvalChallenge(items, screen) {
|
|
1487
|
+
if (items.length === 0) throw new WebAuthnError("an approval covers at least one object");
|
|
1488
|
+
const body = {
|
|
1489
|
+
set: items.map(([t, h]) => ({ t, h })),
|
|
1490
|
+
screen: (0, import_node_crypto4.createHash)("sha256").update(screen, "utf8").digest("hex")
|
|
1491
|
+
};
|
|
1492
|
+
let data;
|
|
1493
|
+
try {
|
|
1494
|
+
data = canonical(body);
|
|
1495
|
+
} catch (err) {
|
|
1496
|
+
if (err instanceof SignedObjectError) throw new WebAuthnError(`bad approval set: ${err.message}`);
|
|
1497
|
+
throw err;
|
|
1498
|
+
}
|
|
1499
|
+
return sha256(new TextEncoder().encode("UAM-APPROVAL-v1\n"), data);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
702
1502
|
// src/version.ts
|
|
703
|
-
var SDK_VERSION = "0.
|
|
1503
|
+
var SDK_VERSION = "0.4.0";
|
|
704
1504
|
|
|
705
1505
|
// src/sdk/config.ts
|
|
706
1506
|
var import_node_os = require("os");
|
|
@@ -870,11 +1670,11 @@ var KeyManager = class {
|
|
|
870
1670
|
/**
|
|
871
1671
|
* Store the address `relayUrl` gave this agent.
|
|
872
1672
|
*/
|
|
873
|
-
saveAddress(name,
|
|
1673
|
+
saveAddress(name, address2, relayUrl) {
|
|
874
1674
|
(0, import_node_fs.mkdirSync)(this._keyDir, { recursive: true });
|
|
875
1675
|
(0, import_node_fs.writeFileSync)(
|
|
876
1676
|
(0, import_node_path2.join)(this._keyDir, `${name}.address`),
|
|
877
|
-
JSON.stringify({ address, relay_url: relayUrl })
|
|
1677
|
+
JSON.stringify({ address: address2, relay_url: relayUrl })
|
|
878
1678
|
);
|
|
879
1679
|
}
|
|
880
1680
|
/**
|
|
@@ -887,14 +1687,14 @@ var KeyManager = class {
|
|
|
887
1687
|
try {
|
|
888
1688
|
const path = (0, import_node_path2.join)(this._keyDir, `${name}.address`);
|
|
889
1689
|
const saved = JSON.parse((0, import_node_fs.readFileSync)(path, "utf-8"));
|
|
890
|
-
const
|
|
1690
|
+
const address2 = saved?.address;
|
|
891
1691
|
const savedRelay = saved?.relay_url;
|
|
892
|
-
if (typeof
|
|
1692
|
+
if (typeof address2 !== "string" || typeof savedRelay !== "string") {
|
|
893
1693
|
return null;
|
|
894
1694
|
}
|
|
895
1695
|
if (trimRelayUrl(savedRelay) !== trimRelayUrl(relayUrl)) return null;
|
|
896
|
-
parseAddress(
|
|
897
|
-
return
|
|
1696
|
+
parseAddress(address2);
|
|
1697
|
+
return address2;
|
|
898
1698
|
} catch {
|
|
899
1699
|
return null;
|
|
900
1700
|
}
|
|
@@ -962,6 +1762,42 @@ var KeyManager = class {
|
|
|
962
1762
|
}
|
|
963
1763
|
};
|
|
964
1764
|
|
|
1765
|
+
// src/sdk/message.ts
|
|
1766
|
+
var SEND_STATUSES = [
|
|
1767
|
+
"delivered",
|
|
1768
|
+
"stored",
|
|
1769
|
+
"relayed",
|
|
1770
|
+
"queued",
|
|
1771
|
+
"duplicate",
|
|
1772
|
+
"dropped",
|
|
1773
|
+
"pending",
|
|
1774
|
+
"accepted",
|
|
1775
|
+
"unknown"
|
|
1776
|
+
];
|
|
1777
|
+
function sendStatus(answer) {
|
|
1778
|
+
if (answer === null) return "unknown";
|
|
1779
|
+
const status = answer["status"];
|
|
1780
|
+
if (typeof status === "string" && SEND_STATUSES.includes(status) && status !== "accepted" && status !== "unknown") {
|
|
1781
|
+
return status;
|
|
1782
|
+
}
|
|
1783
|
+
return "accepted";
|
|
1784
|
+
}
|
|
1785
|
+
function supports(features, name, version = 1) {
|
|
1786
|
+
const prefix = `${name}:`;
|
|
1787
|
+
for (const feature of features) {
|
|
1788
|
+
if (feature === name && version <= 1) return true;
|
|
1789
|
+
if (feature.startsWith(prefix)) {
|
|
1790
|
+
const rest = feature.slice(prefix.length);
|
|
1791
|
+
if (/^\d+$/.test(rest) && Number(rest) >= version) return true;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
return false;
|
|
1795
|
+
}
|
|
1796
|
+
var PENDING_NOTE = "Not delivered: {to} only accepts messages from approved contacts. Do not resend it.";
|
|
1797
|
+
function pendingNote(to) {
|
|
1798
|
+
return PENDING_NOTE.replace("{to}", to);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
965
1801
|
// src/sdk/contact-book.ts
|
|
966
1802
|
var import_node_fs3 = require("fs");
|
|
967
1803
|
var import_node_path3 = require("path");
|
|
@@ -1059,36 +1895,36 @@ var ContactBook = class {
|
|
|
1059
1895
|
/**
|
|
1060
1896
|
* Check if an address is in the contact book (in-memory, no I/O).
|
|
1061
1897
|
*/
|
|
1062
|
-
isKnown(
|
|
1063
|
-
return this._knownAddresses.has(
|
|
1898
|
+
isKnown(address2) {
|
|
1899
|
+
return this._knownAddresses.has(address2);
|
|
1064
1900
|
}
|
|
1065
1901
|
/**
|
|
1066
1902
|
* Check if an address matches any blocked pattern (O(1) set lookup).
|
|
1067
1903
|
*/
|
|
1068
|
-
isBlocked(
|
|
1069
|
-
if (this._blockedExact.has(
|
|
1904
|
+
isBlocked(address2) {
|
|
1905
|
+
if (this._blockedExact.has(address2)) {
|
|
1070
1906
|
return true;
|
|
1071
1907
|
}
|
|
1072
|
-
if (
|
|
1073
|
-
const
|
|
1074
|
-
return this._blockedDomains.has(
|
|
1908
|
+
if (address2.includes("::")) {
|
|
1909
|
+
const domain2 = address2.split("::")[1];
|
|
1910
|
+
return this._blockedDomains.has(domain2);
|
|
1075
1911
|
}
|
|
1076
1912
|
return false;
|
|
1077
1913
|
}
|
|
1078
1914
|
/**
|
|
1079
1915
|
* Look up the public key for a known contact.
|
|
1080
1916
|
*/
|
|
1081
|
-
getPublicKey(
|
|
1917
|
+
getPublicKey(address2) {
|
|
1082
1918
|
if (this._db === null) return null;
|
|
1083
|
-
const row = this._db.prepare("SELECT public_key FROM contacts WHERE address = ?").get(
|
|
1919
|
+
const row = this._db.prepare("SELECT public_key FROM contacts WHERE address = ?").get(address2);
|
|
1084
1920
|
return row?.public_key ?? null;
|
|
1085
1921
|
}
|
|
1086
1922
|
/**
|
|
1087
1923
|
* Return the relay URLs for a known contact (CARD-04).
|
|
1088
1924
|
*/
|
|
1089
|
-
getRelayUrls(
|
|
1925
|
+
getRelayUrls(address2) {
|
|
1090
1926
|
if (this._db === null) return null;
|
|
1091
|
-
const row = this._db.prepare("SELECT relay, relays_json FROM contacts WHERE address = ?").get(
|
|
1927
|
+
const row = this._db.prepare("SELECT relay, relays_json FROM contacts WHERE address = ?").get(address2);
|
|
1092
1928
|
if (!row) return null;
|
|
1093
1929
|
if (row.relays_json !== null) {
|
|
1094
1930
|
return JSON.parse(row.relays_json);
|
|
@@ -1109,19 +1945,19 @@ var ContactBook = class {
|
|
|
1109
1945
|
* @throws {KeyPinningError} If a pinned/verified contact's key would be
|
|
1110
1946
|
* replaced by a different key without `force`.
|
|
1111
1947
|
*/
|
|
1112
|
-
addContact(
|
|
1948
|
+
addContact(address2, publicKey, options) {
|
|
1113
1949
|
if (this._db === null) {
|
|
1114
1950
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1115
1951
|
}
|
|
1116
|
-
const existing = this._db.prepare("SELECT public_key, trust_state FROM contacts WHERE address = ?").get(
|
|
1952
|
+
const existing = this._db.prepare("SELECT public_key, trust_state FROM contacts WHERE address = ?").get(address2);
|
|
1117
1953
|
if (existing !== void 0 && (existing.trust_state === "pinned" || existing.trust_state === "verified") && existing.public_key !== publicKey) {
|
|
1118
1954
|
if (!options?.force) {
|
|
1119
1955
|
throw new KeyPinningError(
|
|
1120
|
-
`Refusing to overwrite ${existing.trust_state} contact ${JSON.stringify(
|
|
1956
|
+
`Refusing to overwrite ${existing.trust_state} contact ${JSON.stringify(address2)}: stored public key does not match the new key. Pass force: true to rotate.`
|
|
1121
1957
|
);
|
|
1122
1958
|
}
|
|
1123
1959
|
log.warn(
|
|
1124
|
-
`Force-replacing the key of ${existing.trust_state} contact ${
|
|
1960
|
+
`Force-replacing the key of ${existing.trust_state} contact ${address2} (key rotation)`
|
|
1125
1961
|
);
|
|
1126
1962
|
}
|
|
1127
1963
|
const displayName = options?.displayName ?? null;
|
|
@@ -1140,8 +1976,8 @@ var ContactBook = class {
|
|
|
1140
1976
|
relay = COALESCE(excluded.relay, contacts.relay),
|
|
1141
1977
|
relays_json = COALESCE(excluded.relays_json, contacts.relays_json),
|
|
1142
1978
|
last_seen = datetime('now')`
|
|
1143
|
-
).run(
|
|
1144
|
-
this._knownAddresses.add(
|
|
1979
|
+
).run(address2, publicKey, displayName, trustState, trustSource, relay, relaysJson);
|
|
1980
|
+
this._knownAddresses.add(address2);
|
|
1145
1981
|
}
|
|
1146
1982
|
/**
|
|
1147
1983
|
* Return all contacts sorted by last_seen DESC.
|
|
@@ -1162,13 +1998,13 @@ var ContactBook = class {
|
|
|
1162
1998
|
/**
|
|
1163
1999
|
* Store a pending handshake request.
|
|
1164
2000
|
*/
|
|
1165
|
-
addPending(
|
|
2001
|
+
addPending(address2, contactCardJson) {
|
|
1166
2002
|
if (this._db === null) {
|
|
1167
2003
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1168
2004
|
}
|
|
1169
2005
|
this._db.prepare(
|
|
1170
2006
|
`INSERT OR REPLACE INTO pending_handshakes (address, contact_card) VALUES (?, ?)`
|
|
1171
|
-
).run(
|
|
2007
|
+
).run(address2, contactCardJson);
|
|
1172
2008
|
}
|
|
1173
2009
|
/**
|
|
1174
2010
|
* Retrieve all pending handshake requests.
|
|
@@ -1187,9 +2023,9 @@ var ContactBook = class {
|
|
|
1187
2023
|
/**
|
|
1188
2024
|
* Remove a pending handshake request.
|
|
1189
2025
|
*/
|
|
1190
|
-
removePending(
|
|
2026
|
+
removePending(address2) {
|
|
1191
2027
|
if (this._db === null) return;
|
|
1192
|
-
this._db.prepare("DELETE FROM pending_handshakes WHERE address = ?").run(
|
|
2028
|
+
this._db.prepare("DELETE FROM pending_handshakes WHERE address = ?").run(address2);
|
|
1193
2029
|
}
|
|
1194
2030
|
/**
|
|
1195
2031
|
* Return pending handshakes older than N days.
|
|
@@ -1209,31 +2045,31 @@ var ContactBook = class {
|
|
|
1209
2045
|
/**
|
|
1210
2046
|
* Return the trust_state for an address, or null if unknown.
|
|
1211
2047
|
*/
|
|
1212
|
-
getTrustState(
|
|
2048
|
+
getTrustState(address2) {
|
|
1213
2049
|
if (this._db === null) return null;
|
|
1214
|
-
const row = this._db.prepare("SELECT trust_state FROM contacts WHERE address = ?").get(
|
|
2050
|
+
const row = this._db.prepare("SELECT trust_state FROM contacts WHERE address = ?").get(address2);
|
|
1215
2051
|
return row?.trust_state ?? null;
|
|
1216
2052
|
}
|
|
1217
2053
|
/**
|
|
1218
2054
|
* Set the pinned_at timestamp and trust_state='pinned' for a contact (TOFU).
|
|
1219
2055
|
*/
|
|
1220
|
-
setPinnedAt(
|
|
2056
|
+
setPinnedAt(address2) {
|
|
1221
2057
|
if (this._db === null) {
|
|
1222
2058
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1223
2059
|
}
|
|
1224
2060
|
this._db.prepare(
|
|
1225
2061
|
"UPDATE contacts SET pinned_at = datetime('now'), trust_state = 'pinned' WHERE address = ?"
|
|
1226
|
-
).run(
|
|
2062
|
+
).run(address2);
|
|
1227
2063
|
}
|
|
1228
2064
|
/**
|
|
1229
2065
|
* Remove a contact by address. Returns true if a contact was deleted.
|
|
1230
2066
|
*/
|
|
1231
|
-
removeContact(
|
|
2067
|
+
removeContact(address2) {
|
|
1232
2068
|
if (this._db === null) {
|
|
1233
2069
|
throw new Error("ContactBook not open. Call open() first.");
|
|
1234
2070
|
}
|
|
1235
|
-
const result = this._db.prepare("DELETE FROM contacts WHERE address = ?").run(
|
|
1236
|
-
this._knownAddresses.delete(
|
|
2071
|
+
const result = this._db.prepare("DELETE FROM contacts WHERE address = ?").run(address2);
|
|
2072
|
+
this._knownAddresses.delete(address2);
|
|
1237
2073
|
return result.changes > 0;
|
|
1238
2074
|
}
|
|
1239
2075
|
/**
|
|
@@ -1241,8 +2077,8 @@ var ContactBook = class {
|
|
|
1241
2077
|
*
|
|
1242
2078
|
* Convenience helper for inbound message filtering (CARD-05, TOFU-02).
|
|
1243
2079
|
*/
|
|
1244
|
-
isTrustedOrVerified(
|
|
1245
|
-
const state = this.getTrustState(
|
|
2080
|
+
isTrustedOrVerified(address2) {
|
|
2081
|
+
const state = this.getTrustState(address2);
|
|
1246
2082
|
return state === "trusted" || state === "verified" || state === "pinned";
|
|
1247
2083
|
}
|
|
1248
2084
|
/**
|
|
@@ -1539,8 +2375,8 @@ var Tier3Resolver = class extends AddressResolver {
|
|
|
1539
2375
|
transport: (0, import_viem.http)(rpcUrl)
|
|
1540
2376
|
});
|
|
1541
2377
|
}
|
|
1542
|
-
async resolvePublicKey(
|
|
1543
|
-
const parsed = parseAddress(
|
|
2378
|
+
async resolvePublicKey(address2, _token, _relayUrl) {
|
|
2379
|
+
const parsed = parseAddress(address2);
|
|
1544
2380
|
const name = parsed.domain;
|
|
1545
2381
|
const cached = this._cache.get(name);
|
|
1546
2382
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -1604,12 +2440,12 @@ var Tier3Resolver = class extends AddressResolver {
|
|
|
1604
2440
|
// src/sdk/resolver.ts
|
|
1605
2441
|
init_dns_verifier();
|
|
1606
2442
|
var Tier1Resolver = class extends AddressResolver {
|
|
1607
|
-
async resolvePublicKey(
|
|
2443
|
+
async resolvePublicKey(address2, _token, relayUrl) {
|
|
1608
2444
|
const resp = await fetch(
|
|
1609
|
-
`${relayUrl}/api/v1/agents/${
|
|
2445
|
+
`${relayUrl}/api/v1/agents/${address2}/public-key`
|
|
1610
2446
|
);
|
|
1611
2447
|
if (resp.status === 404) {
|
|
1612
|
-
throw new UAMError(`Agent not found: ${
|
|
2448
|
+
throw new UAMError(`Agent not found: ${address2}`);
|
|
1613
2449
|
}
|
|
1614
2450
|
if (!resp.ok) {
|
|
1615
2451
|
throw new UAMError(
|
|
@@ -1621,8 +2457,8 @@ var Tier1Resolver = class extends AddressResolver {
|
|
|
1621
2457
|
}
|
|
1622
2458
|
};
|
|
1623
2459
|
var Tier2Resolver = class extends AddressResolver {
|
|
1624
|
-
async resolvePublicKey(
|
|
1625
|
-
const parsed = parseAddress(
|
|
2460
|
+
async resolvePublicKey(address2, _token, _relayUrl) {
|
|
2461
|
+
const parsed = parseAddress(address2);
|
|
1626
2462
|
const txtRecords = await queryUamTxt(parsed.domain);
|
|
1627
2463
|
for (const txt of txtRecords) {
|
|
1628
2464
|
const tags = parseUamTxt(txt);
|
|
@@ -1634,7 +2470,7 @@ var Tier2Resolver = class extends AddressResolver {
|
|
|
1634
2470
|
const httpsKey = await resolveKeyViaHttps(parsed.agent, parsed.domain);
|
|
1635
2471
|
if (httpsKey) return httpsKey;
|
|
1636
2472
|
throw new UAMError(
|
|
1637
|
-
`Could not resolve public key for ${
|
|
2473
|
+
`Could not resolve public key for ${address2} via DNS or HTTPS`
|
|
1638
2474
|
);
|
|
1639
2475
|
}
|
|
1640
2476
|
};
|
|
@@ -1650,21 +2486,21 @@ var SmartResolver = class extends AddressResolver {
|
|
|
1650
2486
|
this._tier2 = new Tier2Resolver();
|
|
1651
2487
|
this._tier3 = tier3Config ? new Tier3Resolver(tier3Config) : null;
|
|
1652
2488
|
}
|
|
1653
|
-
async resolvePublicKey(
|
|
1654
|
-
const parsed = parseAddress(
|
|
1655
|
-
const
|
|
1656
|
-
if (
|
|
1657
|
-
return this._tier1.resolvePublicKey(
|
|
2489
|
+
async resolvePublicKey(address2, token, relayUrl) {
|
|
2490
|
+
const parsed = parseAddress(address2);
|
|
2491
|
+
const domain2 = parsed.domain;
|
|
2492
|
+
if (domain2 === this.relayDomain) {
|
|
2493
|
+
return this._tier1.resolvePublicKey(address2, token, relayUrl);
|
|
1658
2494
|
}
|
|
1659
|
-
if (
|
|
1660
|
-
return this._tier2.resolvePublicKey(
|
|
2495
|
+
if (domain2.includes(".")) {
|
|
2496
|
+
return this._tier2.resolvePublicKey(address2, token, relayUrl);
|
|
1661
2497
|
}
|
|
1662
2498
|
if (!this._tier3) {
|
|
1663
2499
|
throw new UAMError(
|
|
1664
|
-
`Tier 3 resolution requires contract config. Cannot resolve dot-free domain: '${
|
|
2500
|
+
`Tier 3 resolution requires contract config. Cannot resolve dot-free domain: '${domain2}'`
|
|
1665
2501
|
);
|
|
1666
2502
|
}
|
|
1667
|
-
return this._tier3.resolvePublicKey(
|
|
2503
|
+
return this._tier3.resolvePublicKey(address2, token, relayUrl);
|
|
1668
2504
|
}
|
|
1669
2505
|
};
|
|
1670
2506
|
|
|
@@ -1673,15 +2509,23 @@ var TransportBase = class {
|
|
|
1673
2509
|
};
|
|
1674
2510
|
|
|
1675
2511
|
// src/sdk/transport/http.ts
|
|
2512
|
+
async function readAnswer(resp) {
|
|
2513
|
+
try {
|
|
2514
|
+
const body = await resp.json();
|
|
2515
|
+
return body !== null && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
2516
|
+
} catch {
|
|
2517
|
+
return {};
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
1676
2520
|
var HTTPTransport = class extends TransportBase {
|
|
1677
2521
|
_relayUrl;
|
|
1678
2522
|
_token;
|
|
1679
2523
|
_address;
|
|
1680
|
-
constructor(relayUrl, token,
|
|
2524
|
+
constructor(relayUrl, token, address2) {
|
|
1681
2525
|
super();
|
|
1682
2526
|
this._relayUrl = relayUrl.replace(/\/+$/, "");
|
|
1683
2527
|
this._token = token;
|
|
1684
|
-
this._address =
|
|
2528
|
+
this._address = address2;
|
|
1685
2529
|
}
|
|
1686
2530
|
async connect() {
|
|
1687
2531
|
}
|
|
@@ -1701,6 +2545,7 @@ var HTTPTransport = class extends TransportBase {
|
|
|
1701
2545
|
`HTTP send failed: ${resp.status} ${resp.statusText}`
|
|
1702
2546
|
);
|
|
1703
2547
|
}
|
|
2548
|
+
return readAnswer(resp);
|
|
1704
2549
|
}
|
|
1705
2550
|
async receive(limit = 50) {
|
|
1706
2551
|
const url = new URL(
|
|
@@ -1733,6 +2578,7 @@ var BASE_DELAY = 1;
|
|
|
1733
2578
|
var MAX_DELAY = 60;
|
|
1734
2579
|
var JITTER_RANGE = 1;
|
|
1735
2580
|
var SEND_SETTLE_SECONDS = 5;
|
|
2581
|
+
var ACK_TIMEOUT_SECONDS = 5;
|
|
1736
2582
|
var WebSocketTransport = class extends TransportBase {
|
|
1737
2583
|
_url;
|
|
1738
2584
|
_token;
|
|
@@ -1748,8 +2594,12 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1748
2594
|
_unanswered = 0;
|
|
1749
2595
|
_settling = null;
|
|
1750
2596
|
_settled = null;
|
|
2597
|
+
/** Sends waiting for the relay's answer, by message_id. */
|
|
2598
|
+
_waiting = /* @__PURE__ */ new Map();
|
|
1751
2599
|
/** @internal How long a close waits, in ms. Replaceable in tests. */
|
|
1752
2600
|
_settleMs = SEND_SETTLE_SECONDS * 1e3;
|
|
2601
|
+
/** @internal How long send() waits for an answer, in ms. Replaceable in tests. */
|
|
2602
|
+
_ackMs = ACK_TIMEOUT_SECONDS * 1e3;
|
|
1753
2603
|
constructor(wsUrl, token, onMessage) {
|
|
1754
2604
|
super();
|
|
1755
2605
|
this._url = wsUrl;
|
|
@@ -1793,13 +2643,42 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1793
2643
|
this._ws = null;
|
|
1794
2644
|
}
|
|
1795
2645
|
this._connected = false;
|
|
2646
|
+
this._abandonWaiting();
|
|
1796
2647
|
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Send an envelope and wait for the relay's answer.
|
|
2650
|
+
*
|
|
2651
|
+
* Resolves with the ack that names the message, or null when none came
|
|
2652
|
+
* within _ackMs or the socket went: the message may still have arrived.
|
|
2653
|
+
* Rejects with SendRefusedError when the relay refuses it by message_id
|
|
2654
|
+
* (relays before 1.1 don't name it, so their refusals end as null).
|
|
2655
|
+
*/
|
|
1797
2656
|
async send(envelope) {
|
|
1798
2657
|
if (!this._ws || this._ws.readyState !== import_ws.default.OPEN) {
|
|
1799
2658
|
throw new Error("WebSocket not connected");
|
|
1800
2659
|
}
|
|
2660
|
+
const messageId = envelope["message_id"];
|
|
2661
|
+
let answer = null;
|
|
2662
|
+
let timer;
|
|
2663
|
+
if (typeof messageId === "string" && !this._waiting.has(messageId)) {
|
|
2664
|
+
answer = new Promise((resolve, reject) => {
|
|
2665
|
+
this._waiting.set(messageId, { resolve, reject });
|
|
2666
|
+
timer = setTimeout(() => {
|
|
2667
|
+
log.warn(
|
|
2668
|
+
`No answer from the relay for ${messageId} within ${this._ackMs / 1e3}s`
|
|
2669
|
+
);
|
|
2670
|
+
this._settle(messageId, null);
|
|
2671
|
+
}, this._ackMs);
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
1801
2674
|
this._ws.send(JSON.stringify(envelope));
|
|
1802
2675
|
this._unanswered += 1;
|
|
2676
|
+
if (answer === null) return null;
|
|
2677
|
+
try {
|
|
2678
|
+
return await answer;
|
|
2679
|
+
} finally {
|
|
2680
|
+
clearTimeout(timer);
|
|
2681
|
+
}
|
|
1803
2682
|
}
|
|
1804
2683
|
async receive(limit = 50) {
|
|
1805
2684
|
const result = this._pending.slice(0, limit);
|
|
@@ -1826,6 +2705,21 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1826
2705
|
});
|
|
1827
2706
|
return this._settling;
|
|
1828
2707
|
}
|
|
2708
|
+
/** Hand the relay's answer (or a refusal) to the send waiting for it. */
|
|
2709
|
+
_settle(messageId, answer, error) {
|
|
2710
|
+
if (typeof messageId !== "string") return;
|
|
2711
|
+
const waiting = this._waiting.get(messageId);
|
|
2712
|
+
if (!waiting) return;
|
|
2713
|
+
this._waiting.delete(messageId);
|
|
2714
|
+
if (error) waiting.reject(error);
|
|
2715
|
+
else waiting.resolve(answer);
|
|
2716
|
+
}
|
|
2717
|
+
/** The socket went: no answers will come for what it carried. */
|
|
2718
|
+
_abandonWaiting() {
|
|
2719
|
+
const waiting = [...this._waiting.values()];
|
|
2720
|
+
this._waiting.clear();
|
|
2721
|
+
for (const w of waiting) w.resolve(null);
|
|
2722
|
+
}
|
|
1829
2723
|
_answered() {
|
|
1830
2724
|
this._unanswered = Math.max(0, this._unanswered - 1);
|
|
1831
2725
|
if (this._unanswered === 0) this._settled?.();
|
|
@@ -1857,6 +2751,7 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1857
2751
|
this._ws = null;
|
|
1858
2752
|
this._unanswered = 0;
|
|
1859
2753
|
this._settled?.();
|
|
2754
|
+
this._abandonWaiting();
|
|
1860
2755
|
if (!this._running) return;
|
|
1861
2756
|
attempt += 1;
|
|
1862
2757
|
const delay = Math.min(BASE_DELAY * Math.pow(2, attempt), MAX_DELAY);
|
|
@@ -1886,16 +2781,28 @@ var WebSocketTransport = class extends TransportBase {
|
|
|
1886
2781
|
}
|
|
1887
2782
|
} else if (msgType === "ack") {
|
|
1888
2783
|
this._answered();
|
|
2784
|
+
this._settle(msg["message_id"], msg);
|
|
2785
|
+
} else if (msgType === "relay.notice") {
|
|
2786
|
+
log.warn(
|
|
2787
|
+
`Relay notice: message ${String(msg["message_id"])} is ${String(msg["status"])}`
|
|
2788
|
+
);
|
|
1889
2789
|
} else if (msgType === "error" || "error" in msg) {
|
|
1890
2790
|
this._answered();
|
|
2791
|
+
const code = String(msg["error"] ?? msg["code"] ?? "error");
|
|
2792
|
+
const detail = typeof msg["detail"] === "string" ? msg["detail"] : void 0;
|
|
2793
|
+
log.warn(`Relay error: [${code}] ${detail ?? ""}`.trimEnd());
|
|
2794
|
+
const messageId = msg["message_id"];
|
|
2795
|
+
if (typeof messageId === "string") {
|
|
2796
|
+
this._settle(messageId, null, new SendRefusedError(messageId, code, detail));
|
|
2797
|
+
}
|
|
1891
2798
|
}
|
|
1892
2799
|
}
|
|
1893
2800
|
};
|
|
1894
2801
|
|
|
1895
2802
|
// src/sdk/transport/index.ts
|
|
1896
|
-
function createTransport(config, token,
|
|
2803
|
+
function createTransport(config, token, address2, onMessage) {
|
|
1897
2804
|
if (config.transportType === "http") {
|
|
1898
|
-
return new HTTPTransport(config.relayUrl, token,
|
|
2805
|
+
return new HTTPTransport(config.relayUrl, token, address2);
|
|
1899
2806
|
} else {
|
|
1900
2807
|
return new WebSocketTransport(config.relayWsUrl, token, onMessage);
|
|
1901
2808
|
}
|
|
@@ -2288,7 +3195,12 @@ var Agent = class {
|
|
|
2288
3195
|
}
|
|
2289
3196
|
// -- Messaging -----------------------------------------------------------
|
|
2290
3197
|
/**
|
|
2291
|
-
* Send an encrypted, signed message.
|
|
3198
|
+
* Send an encrypted, signed message.
|
|
3199
|
+
*
|
|
3200
|
+
* Returns the message id with what the relay did with it: `status`
|
|
3201
|
+
* "pending" means the recipient only takes approved contacts, and the
|
|
3202
|
+
* message was not kept (don't resend it). `delivered` is true only when
|
|
3203
|
+
* the relay says the recipient has it.
|
|
2292
3204
|
*/
|
|
2293
3205
|
async send(toAddress, message, options) {
|
|
2294
3206
|
await this._ensureConnected();
|
|
@@ -2320,14 +3232,20 @@ var Agent = class {
|
|
|
2320
3232
|
);
|
|
2321
3233
|
const wire = toWireDict(envelope);
|
|
2322
3234
|
const relayUrls = await this._getRelayUrls(toAddress);
|
|
3235
|
+
let answer;
|
|
2323
3236
|
if (relayUrls !== null && relayUrls.length === 1 && relayUrls[0] === this._config.relayUrl) {
|
|
2324
|
-
await this._transport.send(wire);
|
|
3237
|
+
answer = await this._transport.send(wire);
|
|
2325
3238
|
} else if (relayUrls !== null && relayUrls.length > 0) {
|
|
2326
|
-
await this._trySendWithFailover(wire, relayUrls);
|
|
3239
|
+
answer = await this._trySendWithFailover(wire, relayUrls);
|
|
2327
3240
|
} else {
|
|
2328
|
-
await this._transport.send(wire);
|
|
3241
|
+
answer = await this._transport.send(wire);
|
|
2329
3242
|
}
|
|
2330
|
-
|
|
3243
|
+
const status = sendStatus(answer);
|
|
3244
|
+
return {
|
|
3245
|
+
messageId: envelope.messageId,
|
|
3246
|
+
status,
|
|
3247
|
+
delivered: status === "delivered"
|
|
3248
|
+
};
|
|
2331
3249
|
}
|
|
2332
3250
|
/**
|
|
2333
3251
|
* Retrieve, decrypt, and verify pending messages.
|
|
@@ -2372,43 +3290,43 @@ var Agent = class {
|
|
|
2372
3290
|
* Adds the sender to contacts with trust_source='explicit-approval'
|
|
2373
3291
|
* and sends handshake.accept back.
|
|
2374
3292
|
*/
|
|
2375
|
-
async approve(
|
|
3293
|
+
async approve(address2) {
|
|
2376
3294
|
await this._ensureConnected();
|
|
2377
3295
|
const pendingList = this._contactBook.getPending();
|
|
2378
|
-
const entry = pendingList.find((p) => p.address ===
|
|
3296
|
+
const entry = pendingList.find((p) => p.address === address2);
|
|
2379
3297
|
if (!entry) {
|
|
2380
|
-
throw new UAMError(`No pending handshake from ${
|
|
3298
|
+
throw new UAMError(`No pending handshake from ${address2}`);
|
|
2381
3299
|
}
|
|
2382
3300
|
const cardDict = JSON.parse(entry.contactCard);
|
|
2383
3301
|
const card = contactCardFromDict(cardDict);
|
|
2384
3302
|
verifyContactCard(card);
|
|
2385
3303
|
const senderVk = deserializeVerifyKey(card.publicKey);
|
|
2386
|
-
bindCardToEnvelope(card, { fromAddress:
|
|
3304
|
+
bindCardToEnvelope(card, { fromAddress: address2 }, senderVk);
|
|
2387
3305
|
this._contactBook.addContact(card.address, card.publicKey, {
|
|
2388
3306
|
displayName: card.displayName,
|
|
2389
3307
|
trustState: "trusted",
|
|
2390
3308
|
trustSource: "explicit-approval"
|
|
2391
3309
|
});
|
|
2392
|
-
this._contactBook.removePending(
|
|
2393
|
-
await this._handshake._sendAccept(this,
|
|
3310
|
+
this._contactBook.removePending(address2);
|
|
3311
|
+
await this._handshake._sendAccept(this, address2, senderVk);
|
|
2394
3312
|
}
|
|
2395
3313
|
/**
|
|
2396
3314
|
* Deny a pending handshake request.
|
|
2397
3315
|
*
|
|
2398
3316
|
* Removes the entry from pending and sends handshake.deny.
|
|
2399
3317
|
*/
|
|
2400
|
-
async deny(
|
|
3318
|
+
async deny(address2) {
|
|
2401
3319
|
await this._ensureConnected();
|
|
2402
3320
|
const pendingList = this._contactBook.getPending();
|
|
2403
|
-
const entry = pendingList.find((p) => p.address ===
|
|
3321
|
+
const entry = pendingList.find((p) => p.address === address2);
|
|
2404
3322
|
if (!entry) {
|
|
2405
|
-
throw new UAMError(`No pending handshake from ${
|
|
3323
|
+
throw new UAMError(`No pending handshake from ${address2}`);
|
|
2406
3324
|
}
|
|
2407
3325
|
const cardDict = JSON.parse(entry.contactCard);
|
|
2408
3326
|
const card = contactCardFromDict(cardDict);
|
|
2409
3327
|
const senderVk = deserializeVerifyKey(card.publicKey);
|
|
2410
|
-
this._contactBook.removePending(
|
|
2411
|
-
await this._handshake._sendDeny(this,
|
|
3328
|
+
this._contactBook.removePending(address2);
|
|
3329
|
+
await this._handshake._sendDeny(this, address2, senderVk);
|
|
2412
3330
|
}
|
|
2413
3331
|
/**
|
|
2414
3332
|
* Block an address or domain pattern (HAND-04).
|
|
@@ -2434,7 +3352,7 @@ var Agent = class {
|
|
|
2434
3352
|
*
|
|
2435
3353
|
* Returns `true` if verified, `false` on timeout.
|
|
2436
3354
|
*/
|
|
2437
|
-
async verifyDomain(
|
|
3355
|
+
async verifyDomain(domain2, options) {
|
|
2438
3356
|
const { generateTxtRecord: generateTxtRecord2 } = await Promise.resolve().then(() => (init_dns_verifier(), dns_verifier_exports));
|
|
2439
3357
|
await this._ensureConnected();
|
|
2440
3358
|
const timeout = options?.timeout ?? 3e5;
|
|
@@ -2455,7 +3373,7 @@ var Agent = class {
|
|
|
2455
3373
|
"Content-Type": "application/json",
|
|
2456
3374
|
Authorization: `Bearer ${this._token}`
|
|
2457
3375
|
},
|
|
2458
|
-
body: JSON.stringify({ domain }),
|
|
3376
|
+
body: JSON.stringify({ domain: domain2 }),
|
|
2459
3377
|
signal: AbortSignal.timeout(3e4)
|
|
2460
3378
|
}
|
|
2461
3379
|
);
|
|
@@ -2508,7 +3426,7 @@ var Agent = class {
|
|
|
2508
3426
|
if (!resp.ok) {
|
|
2509
3427
|
throw new Error(`HTTP ${resp.status}`);
|
|
2510
3428
|
}
|
|
2511
|
-
return;
|
|
3429
|
+
return await readAnswer(resp);
|
|
2512
3430
|
} catch (err) {
|
|
2513
3431
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2514
3432
|
}
|
|
@@ -2806,13 +3724,13 @@ var Agent = class {
|
|
|
2806
3724
|
};
|
|
2807
3725
|
let resp = await this._postRegistration(body);
|
|
2808
3726
|
if (resp.status === 409) {
|
|
2809
|
-
const
|
|
3727
|
+
const address3 = await this._addressOnRelay();
|
|
2810
3728
|
resp = await this._postRegistration({
|
|
2811
3729
|
...body,
|
|
2812
|
-
proof: this._registrationProof(
|
|
3730
|
+
proof: this._registrationProof(address3)
|
|
2813
3731
|
});
|
|
2814
3732
|
if (resp.status === 409 || resp.status === 403) {
|
|
2815
|
-
throw this._ownershipNotProven(
|
|
3733
|
+
throw this._ownershipNotProven(address3);
|
|
2816
3734
|
}
|
|
2817
3735
|
}
|
|
2818
3736
|
if (!resp.ok) {
|
|
@@ -2822,26 +3740,26 @@ var Agent = class {
|
|
|
2822
3740
|
);
|
|
2823
3741
|
}
|
|
2824
3742
|
const data = await resp.json();
|
|
2825
|
-
const
|
|
2826
|
-
this._useAddress(
|
|
3743
|
+
const address2 = data["address"];
|
|
3744
|
+
this._useAddress(address2);
|
|
2827
3745
|
this._token = data["token"];
|
|
2828
3746
|
const { name } = this._config;
|
|
2829
3747
|
const token = this._token;
|
|
2830
3748
|
this._tryToSave("token", () => this._keyManager.saveToken(name, token));
|
|
2831
|
-
this._saveAddress(
|
|
3749
|
+
this._saveAddress(address2);
|
|
2832
3750
|
}
|
|
2833
3751
|
/** This agent's address on the relay, for signing a proof. */
|
|
2834
3752
|
async _addressOnRelay() {
|
|
2835
3753
|
const { name, explicitRelayDomain, relayDomain, relayUrl } = this._config;
|
|
2836
3754
|
if (explicitRelayDomain !== null) return `${name}::${explicitRelayDomain}`;
|
|
2837
|
-
const
|
|
2838
|
-
if (
|
|
2839
|
-
if (!this._relayMayUse(
|
|
3755
|
+
const domain2 = await this._fetchRelayDomain();
|
|
3756
|
+
if (domain2 === null) return `${name}::${relayDomain}`;
|
|
3757
|
+
if (!this._relayMayUse(domain2)) {
|
|
2840
3758
|
throw new UAMError(
|
|
2841
|
-
`The relay at ${relayUrl} says its domain is ${
|
|
3759
|
+
`The relay at ${relayUrl} says its domain is ${domain2}. Pass the domain option as ${JSON.stringify(domain2)} if that is right.`
|
|
2842
3760
|
);
|
|
2843
3761
|
}
|
|
2844
|
-
return `${name}::${
|
|
3762
|
+
return `${name}::${domain2}`;
|
|
2845
3763
|
}
|
|
2846
3764
|
/**
|
|
2847
3765
|
* True if the relay's URL fits `domain`, so a proof can be signed for it.
|
|
@@ -2849,14 +3767,14 @@ var Agent = class {
|
|
|
2849
3767
|
* A proof names an address on one relay, so it is only signed for a domain
|
|
2850
3768
|
* this URL belongs to. A relay on this machine may use any domain.
|
|
2851
3769
|
*/
|
|
2852
|
-
_relayMayUse(
|
|
3770
|
+
_relayMayUse(domain2) {
|
|
2853
3771
|
let host;
|
|
2854
3772
|
try {
|
|
2855
3773
|
host = new URL(this._config.relayUrl).hostname.toLowerCase();
|
|
2856
3774
|
} catch {
|
|
2857
3775
|
return false;
|
|
2858
3776
|
}
|
|
2859
|
-
const wanted =
|
|
3777
|
+
const wanted = domain2.toLowerCase();
|
|
2860
3778
|
return host === wanted || host.endsWith(`.${wanted}`) || host === "localhost" || host === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
2861
3779
|
}
|
|
2862
3780
|
_postRegistration(body) {
|
|
@@ -2871,27 +3789,27 @@ var Agent = class {
|
|
|
2871
3789
|
*
|
|
2872
3790
|
* The signed string must stay byte-identical to the Python SDK's.
|
|
2873
3791
|
*/
|
|
2874
|
-
_registrationProof(
|
|
3792
|
+
_registrationProof(address2) {
|
|
2875
3793
|
const timestamp = utcTimestamp();
|
|
2876
3794
|
const signature = signMessage(
|
|
2877
3795
|
new TextEncoder().encode(`uam-register-v1
|
|
2878
|
-
${
|
|
3796
|
+
${address2}
|
|
2879
3797
|
${timestamp}`),
|
|
2880
3798
|
this._keyManager.signingKey
|
|
2881
3799
|
);
|
|
2882
3800
|
return { timestamp, signature };
|
|
2883
3801
|
}
|
|
2884
|
-
_ownershipNotProven(
|
|
3802
|
+
_ownershipNotProven(address2) {
|
|
2885
3803
|
return new UAMError(
|
|
2886
|
-
`Address already registered, and this key could not prove ownership: ` +
|
|
3804
|
+
`Address already registered, and this key could not prove ownership: ` + address2
|
|
2887
3805
|
);
|
|
2888
3806
|
}
|
|
2889
3807
|
/** Remember `address` for this relay, if the key folder allows it. */
|
|
2890
|
-
_saveAddress(
|
|
3808
|
+
_saveAddress(address2) {
|
|
2891
3809
|
const { name, relayUrl } = this._config;
|
|
2892
3810
|
this._tryToSave(
|
|
2893
3811
|
"address",
|
|
2894
|
-
() => this._keyManager.saveAddress(name,
|
|
3812
|
+
() => this._keyManager.saveAddress(name, address2, relayUrl)
|
|
2895
3813
|
);
|
|
2896
3814
|
}
|
|
2897
3815
|
/** Saving is best effort: a key folder we can't write doesn't stop us. */
|
|
@@ -2904,12 +3822,12 @@ ${timestamp}`),
|
|
|
2904
3822
|
}
|
|
2905
3823
|
}
|
|
2906
3824
|
/** Take `address` as ours; its domain is this relay's real domain. */
|
|
2907
|
-
_useAddress(
|
|
2908
|
-
this._address =
|
|
2909
|
-
const
|
|
2910
|
-
this._config.relayDomain =
|
|
3825
|
+
_useAddress(address2) {
|
|
3826
|
+
this._address = address2;
|
|
3827
|
+
const domain2 = parseAddress(address2).domain;
|
|
3828
|
+
this._config.relayDomain = domain2;
|
|
2911
3829
|
if (this._resolver instanceof SmartResolver) {
|
|
2912
|
-
this._resolver.relayDomain =
|
|
3830
|
+
this._resolver.relayDomain = domain2;
|
|
2913
3831
|
}
|
|
2914
3832
|
}
|
|
2915
3833
|
/**
|
|
@@ -2924,11 +3842,11 @@ ${timestamp}`),
|
|
|
2924
3842
|
if (explicitRelayDomain !== null) return `${name}::${explicitRelayDomain}`;
|
|
2925
3843
|
const saved = this._keyManager.loadAddress(name, this._config.relayUrl);
|
|
2926
3844
|
if (saved) return saved;
|
|
2927
|
-
const
|
|
2928
|
-
if (
|
|
2929
|
-
const
|
|
2930
|
-
this._saveAddress(
|
|
2931
|
-
return
|
|
3845
|
+
const domain2 = await this._fetchRelayDomain();
|
|
3846
|
+
if (domain2 === null) return `${name}::${this._config.relayDomain}`;
|
|
3847
|
+
const address2 = `${name}::${domain2}`;
|
|
3848
|
+
this._saveAddress(address2);
|
|
3849
|
+
return address2;
|
|
2932
3850
|
}
|
|
2933
3851
|
/** Ask the relay for its domain; null if it cannot say. */
|
|
2934
3852
|
async _fetchRelayDomain() {
|
|
@@ -2937,9 +3855,9 @@ ${timestamp}`),
|
|
|
2937
3855
|
const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
2938
3856
|
if (!resp.ok) return null;
|
|
2939
3857
|
const data = await resp.json();
|
|
2940
|
-
const
|
|
2941
|
-
if (typeof
|
|
2942
|
-
return parseAddress(`${this._config.name}::${
|
|
3858
|
+
const domain2 = data["relay_domain"];
|
|
3859
|
+
if (typeof domain2 !== "string" || domain2 === "") return null;
|
|
3860
|
+
return parseAddress(`${this._config.name}::${domain2}`).domain;
|
|
2943
3861
|
} catch {
|
|
2944
3862
|
return null;
|
|
2945
3863
|
}
|
|
@@ -2947,19 +3865,19 @@ ${timestamp}`),
|
|
|
2947
3865
|
};
|
|
2948
3866
|
|
|
2949
3867
|
// src/sdk/webhook-verify.ts
|
|
2950
|
-
var
|
|
2951
|
-
function verifyWebhookSignature(payload, signatureHeader,
|
|
3868
|
+
var import_node_crypto5 = require("crypto");
|
|
3869
|
+
function verifyWebhookSignature(payload, signatureHeader, secret2) {
|
|
2952
3870
|
if (!signatureHeader.startsWith("sha256=")) {
|
|
2953
3871
|
return false;
|
|
2954
3872
|
}
|
|
2955
|
-
const expected = (0,
|
|
3873
|
+
const expected = (0, import_node_crypto5.createHmac)("sha256", secret2).update(payload).digest("hex");
|
|
2956
3874
|
const received = signatureHeader.slice("sha256=".length);
|
|
2957
3875
|
const expectedBuf = Buffer.from(expected, "utf-8");
|
|
2958
3876
|
const receivedBuf = Buffer.from(received, "utf-8");
|
|
2959
3877
|
if (expectedBuf.length !== receivedBuf.length) {
|
|
2960
3878
|
return false;
|
|
2961
3879
|
}
|
|
2962
|
-
return (0,
|
|
3880
|
+
return (0, import_node_crypto5.timingSafeEqual)(expectedBuf, receivedBuf);
|
|
2963
3881
|
}
|
|
2964
3882
|
|
|
2965
3883
|
// src/sdk/index.ts
|
|
@@ -2968,7 +3886,11 @@ init_dns_verifier();
|
|
|
2968
3886
|
0 && (module.exports = {
|
|
2969
3887
|
Agent,
|
|
2970
3888
|
ContactBook,
|
|
3889
|
+
PENDING_NOTE,
|
|
3890
|
+
SEND_STATUSES,
|
|
3891
|
+
pendingNote,
|
|
2971
3892
|
protocol,
|
|
3893
|
+
supports,
|
|
2972
3894
|
verifyWebhookSignature
|
|
2973
3895
|
});
|
|
2974
3896
|
//# sourceMappingURL=index.cjs.map
|