@youam/sdk 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/dist/cli/index.cjs +777 -65
- package/dist/index.cjs +781 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +217 -14
- package/dist/index.d.ts +217 -14
- package/dist/index.js +778 -50
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SDK configuration (matching Python config.py).
|
|
3
|
-
*
|
|
4
|
-
* Priority (highest wins): constructor arg > env var > default.
|
|
5
|
-
* TOML config file support is Python-only for now (unnecessary complexity for TS MVP).
|
|
6
|
-
*/
|
|
7
1
|
declare class SDKConfig {
|
|
8
2
|
readonly name: string;
|
|
9
3
|
readonly relayUrl: string;
|
|
@@ -13,7 +7,9 @@ declare class SDKConfig {
|
|
|
13
7
|
readonly displayName: string;
|
|
14
8
|
readonly transportType: string;
|
|
15
9
|
readonly trustPolicy: string;
|
|
16
|
-
|
|
10
|
+
relayDomain: string;
|
|
11
|
+
/** The domain the caller chose, if any; null when only derived. */
|
|
12
|
+
readonly explicitRelayDomain: string | null;
|
|
17
13
|
constructor(options: {
|
|
18
14
|
name: string;
|
|
19
15
|
relayUrl?: string | null;
|
|
@@ -34,6 +30,12 @@ declare class SDKConfig {
|
|
|
34
30
|
declare const UAM_VERSION = "0.1";
|
|
35
31
|
/** Maximum envelope size in bytes (64 KB). */
|
|
36
32
|
declare const MAX_ENVELOPE_SIZE = 65536;
|
|
33
|
+
/**
|
|
34
|
+
* Maximum allowed skew, in seconds, between an inbound envelope's signed
|
|
35
|
+
* timestamp and the receiver's clock (either direction). Also the TTL of
|
|
36
|
+
* the inbound replay cache. Matches Python MAX_ENVELOPE_AGE.
|
|
37
|
+
*/
|
|
38
|
+
declare const MAX_ENVELOPE_AGE = 300;
|
|
37
39
|
/** All UAM message types. */
|
|
38
40
|
declare enum MessageType {
|
|
39
41
|
MESSAGE = "message",
|
|
@@ -399,6 +401,7 @@ declare const index_InvalidEnvelopeError: typeof InvalidEnvelopeError;
|
|
|
399
401
|
type index_KeyPinningError = KeyPinningError;
|
|
400
402
|
declare const index_KeyPinningError: typeof KeyPinningError;
|
|
401
403
|
type index_Keypair = Keypair;
|
|
404
|
+
declare const index_MAX_ENVELOPE_AGE: typeof MAX_ENVELOPE_AGE;
|
|
402
405
|
declare const index_MAX_ENVELOPE_SIZE: typeof MAX_ENVELOPE_SIZE;
|
|
403
406
|
type index_MessageEnvelope = MessageEnvelope;
|
|
404
407
|
type index_MessageType = MessageType;
|
|
@@ -442,7 +445,7 @@ declare const index_verifyContactCard: typeof verifyContactCard;
|
|
|
442
445
|
declare const index_verifyEnvelope: typeof verifyEnvelope;
|
|
443
446
|
declare const index_verifySignature: typeof verifySignature;
|
|
444
447
|
declare namespace index {
|
|
445
|
-
export { type index_Address as Address, type index_ContactCard as ContactCard, index_DecryptionError as DecryptionError, index_EncryptionError as EncryptionError, index_EnvelopeTooLargeError as EnvelopeTooLargeError, index_InvalidAddressError as InvalidAddressError, index_InvalidContactCardError as InvalidContactCardError, index_InvalidEnvelopeError as InvalidEnvelopeError, index_KeyPinningError as KeyPinningError, type index_Keypair as Keypair, index_MAX_ENVELOPE_SIZE as MAX_ENVELOPE_SIZE, type index_MessageEnvelope as MessageEnvelope, index_MessageType as MessageType, type index_Seed as Seed, index_SignatureError as SignatureError, index_SignatureVerificationError as SignatureVerificationError, type index_SigningKey as SigningKey, index_UAMError as UAMError, index_UAM_VERSION as UAM_VERSION, type index_VerifyKey as VerifyKey, index_b64Decode as b64Decode, index_b64Encode as b64Encode, index_canonicalize as canonicalize, index_contactCardFromDict as contactCardFromDict, index_contactCardToDict as contactCardToDict, index_createContactCard as createContactCard, index_createEnvelope as createEnvelope, index_decryptPayload as decryptPayload, index_decryptPayloadAnonymous as decryptPayloadAnonymous, index_deserializeSigningKey as deserializeSigningKey, index_deserializeVerifyKey as deserializeVerifyKey, index_encryptPayload as encryptPayload, index_encryptPayloadAnonymous as encryptPayloadAnonymous, index_fromWireDict as fromWireDict, index_generateKeypair as generateKeypair, index_generateNonce as generateNonce, index_parseAddress as parseAddress, index_publicKeyFingerprint as publicKeyFingerprint, index_serializeSigningKey as serializeSigningKey, index_serializeVerifyKey as serializeVerifyKey, index_signMessage as signMessage, index_sodiumReady as sodiumReady, index_toWireDict as toWireDict, index_utcTimestamp as utcTimestamp, index_validateEnvelopeSize as validateEnvelopeSize, index_verifyContactCard as verifyContactCard, index_verifyEnvelope as verifyEnvelope, index_verifySignature as verifySignature };
|
|
448
|
+
export { type index_Address as Address, type index_ContactCard as ContactCard, index_DecryptionError as DecryptionError, index_EncryptionError as EncryptionError, index_EnvelopeTooLargeError as EnvelopeTooLargeError, index_InvalidAddressError as InvalidAddressError, index_InvalidContactCardError as InvalidContactCardError, index_InvalidEnvelopeError as InvalidEnvelopeError, index_KeyPinningError as KeyPinningError, type index_Keypair as Keypair, index_MAX_ENVELOPE_AGE as MAX_ENVELOPE_AGE, index_MAX_ENVELOPE_SIZE as MAX_ENVELOPE_SIZE, type index_MessageEnvelope as MessageEnvelope, index_MessageType as MessageType, type index_Seed as Seed, index_SignatureError as SignatureError, index_SignatureVerificationError as SignatureVerificationError, type index_SigningKey as SigningKey, index_UAMError as UAMError, index_UAM_VERSION as UAM_VERSION, type index_VerifyKey as VerifyKey, index_b64Decode as b64Decode, index_b64Encode as b64Encode, index_canonicalize as canonicalize, index_contactCardFromDict as contactCardFromDict, index_contactCardToDict as contactCardToDict, index_createContactCard as createContactCard, index_createEnvelope as createEnvelope, index_decryptPayload as decryptPayload, index_decryptPayloadAnonymous as decryptPayloadAnonymous, index_deserializeSigningKey as deserializeSigningKey, index_deserializeVerifyKey as deserializeVerifyKey, index_encryptPayload as encryptPayload, index_encryptPayloadAnonymous as encryptPayloadAnonymous, index_fromWireDict as fromWireDict, index_generateKeypair as generateKeypair, index_generateNonce as generateNonce, index_parseAddress as parseAddress, index_publicKeyFingerprint as publicKeyFingerprint, index_serializeSigningKey as serializeSigningKey, index_serializeVerifyKey as serializeVerifyKey, index_signMessage as signMessage, index_sodiumReady as sodiumReady, index_toWireDict as toWireDict, index_utcTimestamp as utcTimestamp, index_validateEnvelopeSize as validateEnvelopeSize, index_verifyContactCard as verifyContactCard, index_verifyEnvelope as verifyEnvelope, index_verifySignature as verifySignature };
|
|
446
449
|
}
|
|
447
450
|
|
|
448
451
|
/**
|
|
@@ -474,13 +477,31 @@ declare class KeyManager {
|
|
|
474
477
|
* Store the relay token alongside the keypair.
|
|
475
478
|
*/
|
|
476
479
|
saveToken(name: string, token: string): void;
|
|
480
|
+
/**
|
|
481
|
+
* Store the address `relayUrl` gave this agent.
|
|
482
|
+
*/
|
|
483
|
+
saveAddress(name: string, address: string, relayUrl: string): void;
|
|
484
|
+
/**
|
|
485
|
+
* Load the address saved for `relayUrl`.
|
|
486
|
+
*
|
|
487
|
+
* Returns null if there is none: the file is missing, unreadable or
|
|
488
|
+
* malformed, or it belongs to another relay.
|
|
489
|
+
*/
|
|
490
|
+
loadAddress(name: string, relayUrl: string): string | null;
|
|
477
491
|
/**
|
|
478
492
|
* Load a previously saved token, or return null.
|
|
479
493
|
*
|
|
480
494
|
* Checks UAM_TOKEN env var first, then file-based storage,
|
|
481
|
-
* then legacy .api_key files for backward compatibility.
|
|
495
|
+
* then legacy .api_key files for backward compatibility. With `relayUrl`,
|
|
496
|
+
* a saved token that belongs to another relay counts as none, so it is
|
|
497
|
+
* only offered to the relay that issued it.
|
|
498
|
+
*/
|
|
499
|
+
loadToken(name: string, relayUrl?: string): string | null;
|
|
500
|
+
/**
|
|
501
|
+
* True if the saved address says this agent's files are another relay's.
|
|
502
|
+
* A missing or damaged file says nothing.
|
|
482
503
|
*/
|
|
483
|
-
|
|
504
|
+
private _savedForAnotherRelay;
|
|
484
505
|
/** Set file permissions to 0o600 (owner read/write only). */
|
|
485
506
|
private _setPermissions;
|
|
486
507
|
/** Warn if key file permissions are too permissive. */
|
|
@@ -583,6 +604,14 @@ declare class ContactBook {
|
|
|
583
604
|
getRelayUrls(address: string): string[] | null;
|
|
584
605
|
/**
|
|
585
606
|
* Add or update a contact (upsert).
|
|
607
|
+
*
|
|
608
|
+
* Refuses to replace the key of a contact whose trust_state is "pinned"
|
|
609
|
+
* or "verified" with a different key, unless `force: true` is passed (for
|
|
610
|
+
* a deliberate key rotation, which is logged). Matches the Python SDK's
|
|
611
|
+
* ContactBook.add_contact.
|
|
612
|
+
*
|
|
613
|
+
* @throws {KeyPinningError} If a pinned/verified contact's key would be
|
|
614
|
+
* replaced by a different key without `force`.
|
|
586
615
|
*/
|
|
587
616
|
addContact(address: string, publicKey: string, options?: {
|
|
588
617
|
displayName?: string | null;
|
|
@@ -590,6 +619,7 @@ declare class ContactBook {
|
|
|
590
619
|
trustSource?: string | null;
|
|
591
620
|
relay?: string | null;
|
|
592
621
|
relays?: string[] | null;
|
|
622
|
+
force?: boolean;
|
|
593
623
|
}): void;
|
|
594
624
|
/**
|
|
595
625
|
* Return all contacts sorted by last_seen DESC.
|
|
@@ -694,6 +724,16 @@ declare class HandshakeManager {
|
|
|
694
724
|
private _handleRequest;
|
|
695
725
|
/**
|
|
696
726
|
* Process a handshake.accept: store the sender as pinned (TOFU).
|
|
727
|
+
*
|
|
728
|
+
* Only honoured when:
|
|
729
|
+
* 1. we previously sent this address a handshake.request
|
|
730
|
+
* (trust_state === "handshake-sent"); anything else is an unsolicited
|
|
731
|
+
* accept and is dropped. This also means an accept can never
|
|
732
|
+
* downgrade a trusted/verified contact to pinned;
|
|
733
|
+
* 2. the payload decrypts with our key and the sender's key, and parses
|
|
734
|
+
* as JSON. Otherwise it is dropped without pinning;
|
|
735
|
+
* 3. an embedded contact_card, if present, is well-formed, correctly
|
|
736
|
+
* self-signed, and bound to the envelope sender (throws otherwise).
|
|
697
737
|
*/
|
|
698
738
|
private _handleAccept;
|
|
699
739
|
/**
|
|
@@ -731,6 +771,55 @@ declare abstract class TransportBase {
|
|
|
731
771
|
abstract listen(callback: (msg: Record<string, unknown>) => Promise<void>): Promise<void>;
|
|
732
772
|
}
|
|
733
773
|
|
|
774
|
+
/**
|
|
775
|
+
* Inbound (from address, message id) pairs kept in a SQLite file.
|
|
776
|
+
*
|
|
777
|
+
* Opens itself on first use, so an Agent driven without connect() still has
|
|
778
|
+
* replay protection. Safe to share between processes: the insert is the
|
|
779
|
+
* check (INSERT OR IGNORE), in WAL mode.
|
|
780
|
+
*/
|
|
781
|
+
declare class PersistentReplayCache {
|
|
782
|
+
readonly capacity: number;
|
|
783
|
+
private readonly _path;
|
|
784
|
+
private readonly _now;
|
|
785
|
+
private _db;
|
|
786
|
+
private _opening;
|
|
787
|
+
private _count;
|
|
788
|
+
private _lastPurge;
|
|
789
|
+
private _createdAt;
|
|
790
|
+
private readonly _overflow;
|
|
791
|
+
/**
|
|
792
|
+
* @param path - The SQLite file
|
|
793
|
+
* @param options.now - The clock, in ms since the epoch
|
|
794
|
+
*/
|
|
795
|
+
constructor(path: string, options?: {
|
|
796
|
+
capacity?: number;
|
|
797
|
+
now?: () => number;
|
|
798
|
+
});
|
|
799
|
+
/** Open the file now, rather than on first use. */
|
|
800
|
+
open(): Promise<void>;
|
|
801
|
+
/**
|
|
802
|
+
* Return true if the envelope must be dropped as a replay. Otherwise record
|
|
803
|
+
* the pair and return false.
|
|
804
|
+
*
|
|
805
|
+
* @param signedAt - The envelope's signed time, in ms since the epoch. It
|
|
806
|
+
* matters only when the cache is full.
|
|
807
|
+
*/
|
|
808
|
+
seenOrRecord(fromAddress: string, messageId: string, signedAt: number): Promise<boolean>;
|
|
809
|
+
/** When this cache file was first created, in ms since the epoch. */
|
|
810
|
+
createdAt(): Promise<number>;
|
|
811
|
+
/** True if an envelope signed at `signedAt` (ms) predates this cache. */
|
|
812
|
+
signedBeforeCache(signedAt: number): Promise<boolean>;
|
|
813
|
+
/** Number of ids currently held on disk. */
|
|
814
|
+
count(): Promise<number>;
|
|
815
|
+
/** Close the file; the next call opens it again. */
|
|
816
|
+
close(): Promise<void>;
|
|
817
|
+
private _open;
|
|
818
|
+
private _connect;
|
|
819
|
+
private _purge;
|
|
820
|
+
private _countRows;
|
|
821
|
+
}
|
|
822
|
+
|
|
734
823
|
/**
|
|
735
824
|
* Agent class -- the primary SDK interface (SDK-01).
|
|
736
825
|
*
|
|
@@ -756,6 +845,23 @@ declare class Agent {
|
|
|
756
845
|
readonly _contactBook: ContactBook;
|
|
757
846
|
/** @internal */
|
|
758
847
|
readonly _handshake: HandshakeManager;
|
|
848
|
+
/**
|
|
849
|
+
* @internal Wall clock in ms since the epoch, used by the inbound
|
|
850
|
+
* freshness/expiry checks and the replay cache. Replaceable in tests.
|
|
851
|
+
*/
|
|
852
|
+
_clock: () => number;
|
|
853
|
+
/**
|
|
854
|
+
* @internal Inbound replay cache, keyed by (from address, message id).
|
|
855
|
+
* Kept on disk under the data dir, so it outlives restarts and is shared by
|
|
856
|
+
* every process using it; capacity from UAM_REPLAY_CACHE_CAPACITY.
|
|
857
|
+
*/
|
|
858
|
+
readonly _replayCache: PersistentReplayCache;
|
|
859
|
+
/** Whether our relay has said it writes relay_meta. */
|
|
860
|
+
private _relayStamps;
|
|
861
|
+
/** Until when, by _monotonic, to take the relay's last no as still true. */
|
|
862
|
+
private _relayNoUntil;
|
|
863
|
+
/** @internal Monotonic clock in ms. Replaceable in tests. */
|
|
864
|
+
_monotonic: () => number;
|
|
759
865
|
/**
|
|
760
866
|
* Create an Agent. No I/O happens here -- call connect() to initialize.
|
|
761
867
|
*/
|
|
@@ -801,7 +907,10 @@ declare class Agent {
|
|
|
801
907
|
* Retrieve, decrypt, and verify pending messages.
|
|
802
908
|
*
|
|
803
909
|
* Returns a list of ReceivedMessage data objects.
|
|
804
|
-
*
|
|
910
|
+
* Drops messages with invalid signatures or decryption errors. A message
|
|
911
|
+
* that fails processing for any other reason (malformed envelope, bad
|
|
912
|
+
* handshake payload, ...) is logged and dropped; it never aborts the
|
|
913
|
+
* rest of the batch.
|
|
805
914
|
*/
|
|
806
915
|
inbox(limit?: number): Promise<ReceivedMessage[]>;
|
|
807
916
|
/** List pending handshake requests. */
|
|
@@ -857,6 +966,10 @@ declare class Agent {
|
|
|
857
966
|
/**
|
|
858
967
|
* Resolve a recipient's public key, checking contact book first.
|
|
859
968
|
*
|
|
969
|
+
* Pure lookup: never writes to the contact book. The contact row is
|
|
970
|
+
* created by _initiateHandshake (as "handshake-sent") once the request
|
|
971
|
+
* has gone out, giving the lifecycle no row -> handshake-sent -> pinned.
|
|
972
|
+
*
|
|
860
973
|
* TOFU-01: Known contacts always use stored key (zero network I/O).
|
|
861
974
|
* TOFU-03: Race-condition double-check after network resolve.
|
|
862
975
|
* Raises KeyPinningError if a pinned contact's key doesn't match.
|
|
@@ -869,6 +982,26 @@ declare class Agent {
|
|
|
869
982
|
* Returns null for invalid messages or handshake protocol messages.
|
|
870
983
|
*/
|
|
871
984
|
private _processInbound;
|
|
985
|
+
/**
|
|
986
|
+
* Recipient, freshness, expiry and replay checks for a signature-verified
|
|
987
|
+
* inbound envelope. Returns false (and logs) when it must be dropped.
|
|
988
|
+
* Matches the Python SDK's _process_inbound checks.
|
|
989
|
+
*/
|
|
990
|
+
private _passesInboundChecks;
|
|
991
|
+
/** True if a message too old to be live is stored mail, not a replay. */
|
|
992
|
+
private _wasStored;
|
|
993
|
+
/**
|
|
994
|
+
* True if our relay writes relay_meta, so a sender can't have.
|
|
995
|
+
*
|
|
996
|
+
* Asked only when a stale message carries a stamp. A yes is remembered for
|
|
997
|
+
* good, a no for RELAY_NO_MEMORY_MS: a relay can be upgraded while this
|
|
998
|
+
* agent runs. A failed lookup isn't remembered, because the relay has
|
|
999
|
+
* already marked this mail delivered and the next message should get a
|
|
1000
|
+
* real answer.
|
|
1001
|
+
*/
|
|
1002
|
+
private _relayStampsMail;
|
|
1003
|
+
/** The relay's features; a body that doesn't list any is an empty list. */
|
|
1004
|
+
private _fetchRelayFeatures;
|
|
872
1005
|
/**
|
|
873
1006
|
* Sweep expired pending handshakes and send receipt.failed (HAND-03).
|
|
874
1007
|
*/
|
|
@@ -883,10 +1016,80 @@ declare class Agent {
|
|
|
883
1016
|
/**
|
|
884
1017
|
* Register with the relay server.
|
|
885
1018
|
*
|
|
886
|
-
* Calls POST /api/v1/register with agent name and public key.
|
|
887
|
-
*
|
|
1019
|
+
* Calls POST /api/v1/register with agent name and public key. If the
|
|
1020
|
+
* address exists (409), repeats it with a proof that this agent holds the
|
|
1021
|
+
* key. Stores the returned API key on disk.
|
|
888
1022
|
*/
|
|
889
1023
|
private _registerWithRelay;
|
|
1024
|
+
/** This agent's address on the relay, for signing a proof. */
|
|
1025
|
+
private _addressOnRelay;
|
|
1026
|
+
/**
|
|
1027
|
+
* True if the relay's URL fits `domain`, so a proof can be signed for it.
|
|
1028
|
+
*
|
|
1029
|
+
* A proof names an address on one relay, so it is only signed for a domain
|
|
1030
|
+
* this URL belongs to. A relay on this machine may use any domain.
|
|
1031
|
+
*/
|
|
1032
|
+
private _relayMayUse;
|
|
1033
|
+
private _postRegistration;
|
|
1034
|
+
/**
|
|
1035
|
+
* Sign a statement that this agent holds the key for `address`.
|
|
1036
|
+
*
|
|
1037
|
+
* The signed string must stay byte-identical to the Python SDK's.
|
|
1038
|
+
*/
|
|
1039
|
+
private _registrationProof;
|
|
1040
|
+
private _ownershipNotProven;
|
|
1041
|
+
/** Remember `address` for this relay, if the key folder allows it. */
|
|
1042
|
+
private _saveAddress;
|
|
1043
|
+
/** Saving is best effort: a key folder we can't write doesn't stop us. */
|
|
1044
|
+
private _tryToSave;
|
|
1045
|
+
/** Take `address` as ours; its domain is this relay's real domain. */
|
|
1046
|
+
private _useAddress;
|
|
1047
|
+
/**
|
|
1048
|
+
* The address the stored token belongs to.
|
|
1049
|
+
*
|
|
1050
|
+
* An explicit domain settles it. Otherwise the relay chooses the domain,
|
|
1051
|
+
* and it need not be the relay's hostname: use the address saved for this
|
|
1052
|
+
* relay, or ask the relay once and keep the answer.
|
|
1053
|
+
*/
|
|
1054
|
+
private _storedAddress;
|
|
1055
|
+
/** Ask the relay for its domain; null if it cannot say. */
|
|
1056
|
+
private _fetchRelayDomain;
|
|
890
1057
|
}
|
|
891
1058
|
|
|
892
|
-
|
|
1059
|
+
/**
|
|
1060
|
+
* Receiver-side webhook signature verification (HOOK-03).
|
|
1061
|
+
*
|
|
1062
|
+
* Small utility for webhook receivers to verify that incoming webhook
|
|
1063
|
+
* payloads were signed by the UAM relay. Uses HMAC-SHA256 with the
|
|
1064
|
+
* agent's webhook secret, which the relay returns when the webhook is set
|
|
1065
|
+
* (PUT /api/v1/agents/{address}/webhook). Setting the same URL again
|
|
1066
|
+
* returns it again.
|
|
1067
|
+
*
|
|
1068
|
+
* Matches Python webhook_verify.py API surface.
|
|
1069
|
+
*
|
|
1070
|
+
* Usage:
|
|
1071
|
+
*
|
|
1072
|
+
* import { verifyWebhookSignature } from "@youam/sdk";
|
|
1073
|
+
*
|
|
1074
|
+
* const payload = Buffer.from(requestBody);
|
|
1075
|
+
* const signature = request.headers["x-uam-signature"];
|
|
1076
|
+
* const secret = "your-webhook-secret";
|
|
1077
|
+
*
|
|
1078
|
+
* if (verifyWebhookSignature(payload, signature, secret)) {
|
|
1079
|
+
* // payload is authentic
|
|
1080
|
+
* }
|
|
1081
|
+
*/
|
|
1082
|
+
/**
|
|
1083
|
+
* Verify an HMAC-SHA256 webhook signature.
|
|
1084
|
+
*
|
|
1085
|
+
* @param payload - Raw request body bytes
|
|
1086
|
+
* @param signatureHeader - "sha256=<hex>" format (X-UAM-Signature header)
|
|
1087
|
+
* @param secret - The agent's webhook secret
|
|
1088
|
+
* @returns `true` if valid, `false` otherwise.
|
|
1089
|
+
*
|
|
1090
|
+
* Uses `crypto.timingSafeEqual()` for constant-time comparison
|
|
1091
|
+
* to prevent timing attacks.
|
|
1092
|
+
*/
|
|
1093
|
+
declare function verifyWebhookSignature(payload: Buffer | Uint8Array, signatureHeader: string, secret: string): boolean;
|
|
1094
|
+
|
|
1095
|
+
export { Agent, ContactBook, type ReceivedMessage, index as protocol, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SDK configuration (matching Python config.py).
|
|
3
|
-
*
|
|
4
|
-
* Priority (highest wins): constructor arg > env var > default.
|
|
5
|
-
* TOML config file support is Python-only for now (unnecessary complexity for TS MVP).
|
|
6
|
-
*/
|
|
7
1
|
declare class SDKConfig {
|
|
8
2
|
readonly name: string;
|
|
9
3
|
readonly relayUrl: string;
|
|
@@ -13,7 +7,9 @@ declare class SDKConfig {
|
|
|
13
7
|
readonly displayName: string;
|
|
14
8
|
readonly transportType: string;
|
|
15
9
|
readonly trustPolicy: string;
|
|
16
|
-
|
|
10
|
+
relayDomain: string;
|
|
11
|
+
/** The domain the caller chose, if any; null when only derived. */
|
|
12
|
+
readonly explicitRelayDomain: string | null;
|
|
17
13
|
constructor(options: {
|
|
18
14
|
name: string;
|
|
19
15
|
relayUrl?: string | null;
|
|
@@ -34,6 +30,12 @@ declare class SDKConfig {
|
|
|
34
30
|
declare const UAM_VERSION = "0.1";
|
|
35
31
|
/** Maximum envelope size in bytes (64 KB). */
|
|
36
32
|
declare const MAX_ENVELOPE_SIZE = 65536;
|
|
33
|
+
/**
|
|
34
|
+
* Maximum allowed skew, in seconds, between an inbound envelope's signed
|
|
35
|
+
* timestamp and the receiver's clock (either direction). Also the TTL of
|
|
36
|
+
* the inbound replay cache. Matches Python MAX_ENVELOPE_AGE.
|
|
37
|
+
*/
|
|
38
|
+
declare const MAX_ENVELOPE_AGE = 300;
|
|
37
39
|
/** All UAM message types. */
|
|
38
40
|
declare enum MessageType {
|
|
39
41
|
MESSAGE = "message",
|
|
@@ -399,6 +401,7 @@ declare const index_InvalidEnvelopeError: typeof InvalidEnvelopeError;
|
|
|
399
401
|
type index_KeyPinningError = KeyPinningError;
|
|
400
402
|
declare const index_KeyPinningError: typeof KeyPinningError;
|
|
401
403
|
type index_Keypair = Keypair;
|
|
404
|
+
declare const index_MAX_ENVELOPE_AGE: typeof MAX_ENVELOPE_AGE;
|
|
402
405
|
declare const index_MAX_ENVELOPE_SIZE: typeof MAX_ENVELOPE_SIZE;
|
|
403
406
|
type index_MessageEnvelope = MessageEnvelope;
|
|
404
407
|
type index_MessageType = MessageType;
|
|
@@ -442,7 +445,7 @@ declare const index_verifyContactCard: typeof verifyContactCard;
|
|
|
442
445
|
declare const index_verifyEnvelope: typeof verifyEnvelope;
|
|
443
446
|
declare const index_verifySignature: typeof verifySignature;
|
|
444
447
|
declare namespace index {
|
|
445
|
-
export { type index_Address as Address, type index_ContactCard as ContactCard, index_DecryptionError as DecryptionError, index_EncryptionError as EncryptionError, index_EnvelopeTooLargeError as EnvelopeTooLargeError, index_InvalidAddressError as InvalidAddressError, index_InvalidContactCardError as InvalidContactCardError, index_InvalidEnvelopeError as InvalidEnvelopeError, index_KeyPinningError as KeyPinningError, type index_Keypair as Keypair, index_MAX_ENVELOPE_SIZE as MAX_ENVELOPE_SIZE, type index_MessageEnvelope as MessageEnvelope, index_MessageType as MessageType, type index_Seed as Seed, index_SignatureError as SignatureError, index_SignatureVerificationError as SignatureVerificationError, type index_SigningKey as SigningKey, index_UAMError as UAMError, index_UAM_VERSION as UAM_VERSION, type index_VerifyKey as VerifyKey, index_b64Decode as b64Decode, index_b64Encode as b64Encode, index_canonicalize as canonicalize, index_contactCardFromDict as contactCardFromDict, index_contactCardToDict as contactCardToDict, index_createContactCard as createContactCard, index_createEnvelope as createEnvelope, index_decryptPayload as decryptPayload, index_decryptPayloadAnonymous as decryptPayloadAnonymous, index_deserializeSigningKey as deserializeSigningKey, index_deserializeVerifyKey as deserializeVerifyKey, index_encryptPayload as encryptPayload, index_encryptPayloadAnonymous as encryptPayloadAnonymous, index_fromWireDict as fromWireDict, index_generateKeypair as generateKeypair, index_generateNonce as generateNonce, index_parseAddress as parseAddress, index_publicKeyFingerprint as publicKeyFingerprint, index_serializeSigningKey as serializeSigningKey, index_serializeVerifyKey as serializeVerifyKey, index_signMessage as signMessage, index_sodiumReady as sodiumReady, index_toWireDict as toWireDict, index_utcTimestamp as utcTimestamp, index_validateEnvelopeSize as validateEnvelopeSize, index_verifyContactCard as verifyContactCard, index_verifyEnvelope as verifyEnvelope, index_verifySignature as verifySignature };
|
|
448
|
+
export { type index_Address as Address, type index_ContactCard as ContactCard, index_DecryptionError as DecryptionError, index_EncryptionError as EncryptionError, index_EnvelopeTooLargeError as EnvelopeTooLargeError, index_InvalidAddressError as InvalidAddressError, index_InvalidContactCardError as InvalidContactCardError, index_InvalidEnvelopeError as InvalidEnvelopeError, index_KeyPinningError as KeyPinningError, type index_Keypair as Keypair, index_MAX_ENVELOPE_AGE as MAX_ENVELOPE_AGE, index_MAX_ENVELOPE_SIZE as MAX_ENVELOPE_SIZE, type index_MessageEnvelope as MessageEnvelope, index_MessageType as MessageType, type index_Seed as Seed, index_SignatureError as SignatureError, index_SignatureVerificationError as SignatureVerificationError, type index_SigningKey as SigningKey, index_UAMError as UAMError, index_UAM_VERSION as UAM_VERSION, type index_VerifyKey as VerifyKey, index_b64Decode as b64Decode, index_b64Encode as b64Encode, index_canonicalize as canonicalize, index_contactCardFromDict as contactCardFromDict, index_contactCardToDict as contactCardToDict, index_createContactCard as createContactCard, index_createEnvelope as createEnvelope, index_decryptPayload as decryptPayload, index_decryptPayloadAnonymous as decryptPayloadAnonymous, index_deserializeSigningKey as deserializeSigningKey, index_deserializeVerifyKey as deserializeVerifyKey, index_encryptPayload as encryptPayload, index_encryptPayloadAnonymous as encryptPayloadAnonymous, index_fromWireDict as fromWireDict, index_generateKeypair as generateKeypair, index_generateNonce as generateNonce, index_parseAddress as parseAddress, index_publicKeyFingerprint as publicKeyFingerprint, index_serializeSigningKey as serializeSigningKey, index_serializeVerifyKey as serializeVerifyKey, index_signMessage as signMessage, index_sodiumReady as sodiumReady, index_toWireDict as toWireDict, index_utcTimestamp as utcTimestamp, index_validateEnvelopeSize as validateEnvelopeSize, index_verifyContactCard as verifyContactCard, index_verifyEnvelope as verifyEnvelope, index_verifySignature as verifySignature };
|
|
446
449
|
}
|
|
447
450
|
|
|
448
451
|
/**
|
|
@@ -474,13 +477,31 @@ declare class KeyManager {
|
|
|
474
477
|
* Store the relay token alongside the keypair.
|
|
475
478
|
*/
|
|
476
479
|
saveToken(name: string, token: string): void;
|
|
480
|
+
/**
|
|
481
|
+
* Store the address `relayUrl` gave this agent.
|
|
482
|
+
*/
|
|
483
|
+
saveAddress(name: string, address: string, relayUrl: string): void;
|
|
484
|
+
/**
|
|
485
|
+
* Load the address saved for `relayUrl`.
|
|
486
|
+
*
|
|
487
|
+
* Returns null if there is none: the file is missing, unreadable or
|
|
488
|
+
* malformed, or it belongs to another relay.
|
|
489
|
+
*/
|
|
490
|
+
loadAddress(name: string, relayUrl: string): string | null;
|
|
477
491
|
/**
|
|
478
492
|
* Load a previously saved token, or return null.
|
|
479
493
|
*
|
|
480
494
|
* Checks UAM_TOKEN env var first, then file-based storage,
|
|
481
|
-
* then legacy .api_key files for backward compatibility.
|
|
495
|
+
* then legacy .api_key files for backward compatibility. With `relayUrl`,
|
|
496
|
+
* a saved token that belongs to another relay counts as none, so it is
|
|
497
|
+
* only offered to the relay that issued it.
|
|
498
|
+
*/
|
|
499
|
+
loadToken(name: string, relayUrl?: string): string | null;
|
|
500
|
+
/**
|
|
501
|
+
* True if the saved address says this agent's files are another relay's.
|
|
502
|
+
* A missing or damaged file says nothing.
|
|
482
503
|
*/
|
|
483
|
-
|
|
504
|
+
private _savedForAnotherRelay;
|
|
484
505
|
/** Set file permissions to 0o600 (owner read/write only). */
|
|
485
506
|
private _setPermissions;
|
|
486
507
|
/** Warn if key file permissions are too permissive. */
|
|
@@ -583,6 +604,14 @@ declare class ContactBook {
|
|
|
583
604
|
getRelayUrls(address: string): string[] | null;
|
|
584
605
|
/**
|
|
585
606
|
* Add or update a contact (upsert).
|
|
607
|
+
*
|
|
608
|
+
* Refuses to replace the key of a contact whose trust_state is "pinned"
|
|
609
|
+
* or "verified" with a different key, unless `force: true` is passed (for
|
|
610
|
+
* a deliberate key rotation, which is logged). Matches the Python SDK's
|
|
611
|
+
* ContactBook.add_contact.
|
|
612
|
+
*
|
|
613
|
+
* @throws {KeyPinningError} If a pinned/verified contact's key would be
|
|
614
|
+
* replaced by a different key without `force`.
|
|
586
615
|
*/
|
|
587
616
|
addContact(address: string, publicKey: string, options?: {
|
|
588
617
|
displayName?: string | null;
|
|
@@ -590,6 +619,7 @@ declare class ContactBook {
|
|
|
590
619
|
trustSource?: string | null;
|
|
591
620
|
relay?: string | null;
|
|
592
621
|
relays?: string[] | null;
|
|
622
|
+
force?: boolean;
|
|
593
623
|
}): void;
|
|
594
624
|
/**
|
|
595
625
|
* Return all contacts sorted by last_seen DESC.
|
|
@@ -694,6 +724,16 @@ declare class HandshakeManager {
|
|
|
694
724
|
private _handleRequest;
|
|
695
725
|
/**
|
|
696
726
|
* Process a handshake.accept: store the sender as pinned (TOFU).
|
|
727
|
+
*
|
|
728
|
+
* Only honoured when:
|
|
729
|
+
* 1. we previously sent this address a handshake.request
|
|
730
|
+
* (trust_state === "handshake-sent"); anything else is an unsolicited
|
|
731
|
+
* accept and is dropped. This also means an accept can never
|
|
732
|
+
* downgrade a trusted/verified contact to pinned;
|
|
733
|
+
* 2. the payload decrypts with our key and the sender's key, and parses
|
|
734
|
+
* as JSON. Otherwise it is dropped without pinning;
|
|
735
|
+
* 3. an embedded contact_card, if present, is well-formed, correctly
|
|
736
|
+
* self-signed, and bound to the envelope sender (throws otherwise).
|
|
697
737
|
*/
|
|
698
738
|
private _handleAccept;
|
|
699
739
|
/**
|
|
@@ -731,6 +771,55 @@ declare abstract class TransportBase {
|
|
|
731
771
|
abstract listen(callback: (msg: Record<string, unknown>) => Promise<void>): Promise<void>;
|
|
732
772
|
}
|
|
733
773
|
|
|
774
|
+
/**
|
|
775
|
+
* Inbound (from address, message id) pairs kept in a SQLite file.
|
|
776
|
+
*
|
|
777
|
+
* Opens itself on first use, so an Agent driven without connect() still has
|
|
778
|
+
* replay protection. Safe to share between processes: the insert is the
|
|
779
|
+
* check (INSERT OR IGNORE), in WAL mode.
|
|
780
|
+
*/
|
|
781
|
+
declare class PersistentReplayCache {
|
|
782
|
+
readonly capacity: number;
|
|
783
|
+
private readonly _path;
|
|
784
|
+
private readonly _now;
|
|
785
|
+
private _db;
|
|
786
|
+
private _opening;
|
|
787
|
+
private _count;
|
|
788
|
+
private _lastPurge;
|
|
789
|
+
private _createdAt;
|
|
790
|
+
private readonly _overflow;
|
|
791
|
+
/**
|
|
792
|
+
* @param path - The SQLite file
|
|
793
|
+
* @param options.now - The clock, in ms since the epoch
|
|
794
|
+
*/
|
|
795
|
+
constructor(path: string, options?: {
|
|
796
|
+
capacity?: number;
|
|
797
|
+
now?: () => number;
|
|
798
|
+
});
|
|
799
|
+
/** Open the file now, rather than on first use. */
|
|
800
|
+
open(): Promise<void>;
|
|
801
|
+
/**
|
|
802
|
+
* Return true if the envelope must be dropped as a replay. Otherwise record
|
|
803
|
+
* the pair and return false.
|
|
804
|
+
*
|
|
805
|
+
* @param signedAt - The envelope's signed time, in ms since the epoch. It
|
|
806
|
+
* matters only when the cache is full.
|
|
807
|
+
*/
|
|
808
|
+
seenOrRecord(fromAddress: string, messageId: string, signedAt: number): Promise<boolean>;
|
|
809
|
+
/** When this cache file was first created, in ms since the epoch. */
|
|
810
|
+
createdAt(): Promise<number>;
|
|
811
|
+
/** True if an envelope signed at `signedAt` (ms) predates this cache. */
|
|
812
|
+
signedBeforeCache(signedAt: number): Promise<boolean>;
|
|
813
|
+
/** Number of ids currently held on disk. */
|
|
814
|
+
count(): Promise<number>;
|
|
815
|
+
/** Close the file; the next call opens it again. */
|
|
816
|
+
close(): Promise<void>;
|
|
817
|
+
private _open;
|
|
818
|
+
private _connect;
|
|
819
|
+
private _purge;
|
|
820
|
+
private _countRows;
|
|
821
|
+
}
|
|
822
|
+
|
|
734
823
|
/**
|
|
735
824
|
* Agent class -- the primary SDK interface (SDK-01).
|
|
736
825
|
*
|
|
@@ -756,6 +845,23 @@ declare class Agent {
|
|
|
756
845
|
readonly _contactBook: ContactBook;
|
|
757
846
|
/** @internal */
|
|
758
847
|
readonly _handshake: HandshakeManager;
|
|
848
|
+
/**
|
|
849
|
+
* @internal Wall clock in ms since the epoch, used by the inbound
|
|
850
|
+
* freshness/expiry checks and the replay cache. Replaceable in tests.
|
|
851
|
+
*/
|
|
852
|
+
_clock: () => number;
|
|
853
|
+
/**
|
|
854
|
+
* @internal Inbound replay cache, keyed by (from address, message id).
|
|
855
|
+
* Kept on disk under the data dir, so it outlives restarts and is shared by
|
|
856
|
+
* every process using it; capacity from UAM_REPLAY_CACHE_CAPACITY.
|
|
857
|
+
*/
|
|
858
|
+
readonly _replayCache: PersistentReplayCache;
|
|
859
|
+
/** Whether our relay has said it writes relay_meta. */
|
|
860
|
+
private _relayStamps;
|
|
861
|
+
/** Until when, by _monotonic, to take the relay's last no as still true. */
|
|
862
|
+
private _relayNoUntil;
|
|
863
|
+
/** @internal Monotonic clock in ms. Replaceable in tests. */
|
|
864
|
+
_monotonic: () => number;
|
|
759
865
|
/**
|
|
760
866
|
* Create an Agent. No I/O happens here -- call connect() to initialize.
|
|
761
867
|
*/
|
|
@@ -801,7 +907,10 @@ declare class Agent {
|
|
|
801
907
|
* Retrieve, decrypt, and verify pending messages.
|
|
802
908
|
*
|
|
803
909
|
* Returns a list of ReceivedMessage data objects.
|
|
804
|
-
*
|
|
910
|
+
* Drops messages with invalid signatures or decryption errors. A message
|
|
911
|
+
* that fails processing for any other reason (malformed envelope, bad
|
|
912
|
+
* handshake payload, ...) is logged and dropped; it never aborts the
|
|
913
|
+
* rest of the batch.
|
|
805
914
|
*/
|
|
806
915
|
inbox(limit?: number): Promise<ReceivedMessage[]>;
|
|
807
916
|
/** List pending handshake requests. */
|
|
@@ -857,6 +966,10 @@ declare class Agent {
|
|
|
857
966
|
/**
|
|
858
967
|
* Resolve a recipient's public key, checking contact book first.
|
|
859
968
|
*
|
|
969
|
+
* Pure lookup: never writes to the contact book. The contact row is
|
|
970
|
+
* created by _initiateHandshake (as "handshake-sent") once the request
|
|
971
|
+
* has gone out, giving the lifecycle no row -> handshake-sent -> pinned.
|
|
972
|
+
*
|
|
860
973
|
* TOFU-01: Known contacts always use stored key (zero network I/O).
|
|
861
974
|
* TOFU-03: Race-condition double-check after network resolve.
|
|
862
975
|
* Raises KeyPinningError if a pinned contact's key doesn't match.
|
|
@@ -869,6 +982,26 @@ declare class Agent {
|
|
|
869
982
|
* Returns null for invalid messages or handshake protocol messages.
|
|
870
983
|
*/
|
|
871
984
|
private _processInbound;
|
|
985
|
+
/**
|
|
986
|
+
* Recipient, freshness, expiry and replay checks for a signature-verified
|
|
987
|
+
* inbound envelope. Returns false (and logs) when it must be dropped.
|
|
988
|
+
* Matches the Python SDK's _process_inbound checks.
|
|
989
|
+
*/
|
|
990
|
+
private _passesInboundChecks;
|
|
991
|
+
/** True if a message too old to be live is stored mail, not a replay. */
|
|
992
|
+
private _wasStored;
|
|
993
|
+
/**
|
|
994
|
+
* True if our relay writes relay_meta, so a sender can't have.
|
|
995
|
+
*
|
|
996
|
+
* Asked only when a stale message carries a stamp. A yes is remembered for
|
|
997
|
+
* good, a no for RELAY_NO_MEMORY_MS: a relay can be upgraded while this
|
|
998
|
+
* agent runs. A failed lookup isn't remembered, because the relay has
|
|
999
|
+
* already marked this mail delivered and the next message should get a
|
|
1000
|
+
* real answer.
|
|
1001
|
+
*/
|
|
1002
|
+
private _relayStampsMail;
|
|
1003
|
+
/** The relay's features; a body that doesn't list any is an empty list. */
|
|
1004
|
+
private _fetchRelayFeatures;
|
|
872
1005
|
/**
|
|
873
1006
|
* Sweep expired pending handshakes and send receipt.failed (HAND-03).
|
|
874
1007
|
*/
|
|
@@ -883,10 +1016,80 @@ declare class Agent {
|
|
|
883
1016
|
/**
|
|
884
1017
|
* Register with the relay server.
|
|
885
1018
|
*
|
|
886
|
-
* Calls POST /api/v1/register with agent name and public key.
|
|
887
|
-
*
|
|
1019
|
+
* Calls POST /api/v1/register with agent name and public key. If the
|
|
1020
|
+
* address exists (409), repeats it with a proof that this agent holds the
|
|
1021
|
+
* key. Stores the returned API key on disk.
|
|
888
1022
|
*/
|
|
889
1023
|
private _registerWithRelay;
|
|
1024
|
+
/** This agent's address on the relay, for signing a proof. */
|
|
1025
|
+
private _addressOnRelay;
|
|
1026
|
+
/**
|
|
1027
|
+
* True if the relay's URL fits `domain`, so a proof can be signed for it.
|
|
1028
|
+
*
|
|
1029
|
+
* A proof names an address on one relay, so it is only signed for a domain
|
|
1030
|
+
* this URL belongs to. A relay on this machine may use any domain.
|
|
1031
|
+
*/
|
|
1032
|
+
private _relayMayUse;
|
|
1033
|
+
private _postRegistration;
|
|
1034
|
+
/**
|
|
1035
|
+
* Sign a statement that this agent holds the key for `address`.
|
|
1036
|
+
*
|
|
1037
|
+
* The signed string must stay byte-identical to the Python SDK's.
|
|
1038
|
+
*/
|
|
1039
|
+
private _registrationProof;
|
|
1040
|
+
private _ownershipNotProven;
|
|
1041
|
+
/** Remember `address` for this relay, if the key folder allows it. */
|
|
1042
|
+
private _saveAddress;
|
|
1043
|
+
/** Saving is best effort: a key folder we can't write doesn't stop us. */
|
|
1044
|
+
private _tryToSave;
|
|
1045
|
+
/** Take `address` as ours; its domain is this relay's real domain. */
|
|
1046
|
+
private _useAddress;
|
|
1047
|
+
/**
|
|
1048
|
+
* The address the stored token belongs to.
|
|
1049
|
+
*
|
|
1050
|
+
* An explicit domain settles it. Otherwise the relay chooses the domain,
|
|
1051
|
+
* and it need not be the relay's hostname: use the address saved for this
|
|
1052
|
+
* relay, or ask the relay once and keep the answer.
|
|
1053
|
+
*/
|
|
1054
|
+
private _storedAddress;
|
|
1055
|
+
/** Ask the relay for its domain; null if it cannot say. */
|
|
1056
|
+
private _fetchRelayDomain;
|
|
890
1057
|
}
|
|
891
1058
|
|
|
892
|
-
|
|
1059
|
+
/**
|
|
1060
|
+
* Receiver-side webhook signature verification (HOOK-03).
|
|
1061
|
+
*
|
|
1062
|
+
* Small utility for webhook receivers to verify that incoming webhook
|
|
1063
|
+
* payloads were signed by the UAM relay. Uses HMAC-SHA256 with the
|
|
1064
|
+
* agent's webhook secret, which the relay returns when the webhook is set
|
|
1065
|
+
* (PUT /api/v1/agents/{address}/webhook). Setting the same URL again
|
|
1066
|
+
* returns it again.
|
|
1067
|
+
*
|
|
1068
|
+
* Matches Python webhook_verify.py API surface.
|
|
1069
|
+
*
|
|
1070
|
+
* Usage:
|
|
1071
|
+
*
|
|
1072
|
+
* import { verifyWebhookSignature } from "@youam/sdk";
|
|
1073
|
+
*
|
|
1074
|
+
* const payload = Buffer.from(requestBody);
|
|
1075
|
+
* const signature = request.headers["x-uam-signature"];
|
|
1076
|
+
* const secret = "your-webhook-secret";
|
|
1077
|
+
*
|
|
1078
|
+
* if (verifyWebhookSignature(payload, signature, secret)) {
|
|
1079
|
+
* // payload is authentic
|
|
1080
|
+
* }
|
|
1081
|
+
*/
|
|
1082
|
+
/**
|
|
1083
|
+
* Verify an HMAC-SHA256 webhook signature.
|
|
1084
|
+
*
|
|
1085
|
+
* @param payload - Raw request body bytes
|
|
1086
|
+
* @param signatureHeader - "sha256=<hex>" format (X-UAM-Signature header)
|
|
1087
|
+
* @param secret - The agent's webhook secret
|
|
1088
|
+
* @returns `true` if valid, `false` otherwise.
|
|
1089
|
+
*
|
|
1090
|
+
* Uses `crypto.timingSafeEqual()` for constant-time comparison
|
|
1091
|
+
* to prevent timing attacks.
|
|
1092
|
+
*/
|
|
1093
|
+
declare function verifyWebhookSignature(payload: Buffer | Uint8Array, signatureHeader: string, secret: string): boolean;
|
|
1094
|
+
|
|
1095
|
+
export { Agent, ContactBook, type ReceivedMessage, index as protocol, verifyWebhookSignature };
|