@youam/sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- readonly relayDomain: string;
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",
@@ -106,6 +108,18 @@ declare class InvalidContactCardError extends UAMError {
106
108
  declare class KeyPinningError extends UAMError {
107
109
  constructor(message?: string);
108
110
  }
111
+ /**
112
+ * The relay refused a message sent over WebSocket.
113
+ *
114
+ * `code` is the relay's error code (e.g. "rate_limited"), `detail` its
115
+ * explanation. Over HTTP the same refusal is an HTTP error.
116
+ */
117
+ declare class SendRefusedError extends UAMError {
118
+ readonly messageId: string;
119
+ readonly code: string;
120
+ readonly detail: string | undefined;
121
+ constructor(messageId: string, code: string, detail?: string);
122
+ }
109
123
 
110
124
  /**
111
125
  * UAM address parsing and validation.
@@ -376,6 +390,119 @@ declare function createContactCard(address: string, displayName: string, relay:
376
390
  */
377
391
  declare function verifyContactCard(card: ContactCard): void;
378
392
 
393
+ /**
394
+ * Signed owner objects: profile, canonical form, hash and schemas.
395
+ *
396
+ * Mirrors uam.protocol.signed in the Python SDK; both are checked against
397
+ * the same vectors (tests/vectors/signed/). Every owner object (key log
398
+ * entries, links, invites, redemptions, grants) is a JSON body in a
399
+ * restricted profile, hashed as
400
+ *
401
+ * h = base64url(SHA-256("UAM-<type>-v1\n" || JCS(body))) (no padding)
402
+ *
403
+ * The profile: printable ASCII strings, integers within +/-(2^53-1), no
404
+ * floats, no nulls, no duplicate keys, and no members a type doesn't define
405
+ * (new ones go under `ext`, with `crit` naming those a verifier must know).
406
+ * A body that breaks any rule is refused, never repaired.
407
+ */
408
+
409
+ type Body = Record<string, unknown>;
410
+ /** A signed-object body outside the profile or its type's schema. */
411
+ declare class SignedObjectError extends UAMError {
412
+ constructor(message?: string);
413
+ }
414
+ /** Parse a body, refusing anything outside the profile. */
415
+ declare function parseBody(text: string | Uint8Array): Body;
416
+ /** RFC 8785 canonical form (inside the profile: sorted keys, no whitespace). */
417
+ declare function canonical(body: Body): Uint8Array;
418
+ /** The object's hash; the body is validated first. */
419
+ declare function objectHash(body: Body): string;
420
+ /** Refuse a body outside the profile or its type's schema. */
421
+ declare function validate(body: unknown): asserts body is Body;
422
+
423
+ /**
424
+ * did:key for the two key types owner objects carry (as the Python SDK).
425
+ *
426
+ * Ed25519 (an agent's key, `did:key:z6Mk...`, multicodec 0xed) and P-256
427
+ * (a passkey, `did:key:zDn...`, multicodec 0x1200, compressed point).
428
+ */
429
+
430
+ type DidKeyKind = "ed25519" | "p256";
431
+ /** A did:key string that isn't one of the two supported key types. */
432
+ declare class InvalidDidKeyError extends UAMError {
433
+ constructor(message?: string);
434
+ }
435
+ /** `did:key:z...` for an Ed25519 key (32 bytes) or a compressed P-256 point (33 bytes). */
436
+ declare function encodeDidKey(kind: DidKeyKind, publicKey: Uint8Array): string;
437
+ /** The key type and raw public key in a did:key string. */
438
+ declare function decodeDidKey(did: string): {
439
+ kind: DidKeyKind;
440
+ key: Uint8Array;
441
+ };
442
+
443
+ /**
444
+ * Passkeys (WebAuthn): registration, assertions and the approval challenge.
445
+ *
446
+ * Mirrors uam.protocol.webauthn in the Python SDK; both are checked against
447
+ * the same vectors (tests/vectors/webauthn/).
448
+ *
449
+ * - registration: `webauthn.create`, attestation format `none` only, user
450
+ * present and verified, ES256 only;
451
+ * - assertion: `webauthn.get`, the exact challenge, the owner origin exactly,
452
+ * no cross-origin frame, rpIdHash = SHA-256(RP ID), user present and
453
+ * verified, backup eligibility unchanged, an ES256 signature over
454
+ * `authenticatorData || SHA-256(clientDataJSON)` (high-S accepted), and,
455
+ * where the stored counter is known, a counter that went up whenever either
456
+ * count is non-zero (synced passkeys report 0 throughout; a count that
457
+ * drops to 0 may be a clone).
458
+ *
459
+ * ES256 only: a passkey's did:key must never look like an agent key.
460
+ */
461
+
462
+ /** A registration or assertion that fails a check. */
463
+ declare class WebAuthnError extends UAMError {
464
+ constructor(message?: string);
465
+ }
466
+ /** A registered passkey: its id, algorithm, compressed P-256 key and flags. */
467
+ interface Credential {
468
+ readonly id: Uint8Array;
469
+ readonly alg: number;
470
+ readonly publicKey: Uint8Array;
471
+ readonly backupEligible: boolean;
472
+ readonly signCount: number;
473
+ }
474
+ declare function credentialDid(credential: Credential): string;
475
+ /** Check a `webauthn.create` response; the passkey it registers. */
476
+ declare function verifyRegistration(args: {
477
+ attestationObject: Uint8Array;
478
+ clientDataJSON: Uint8Array;
479
+ challenge: Uint8Array;
480
+ origin: string;
481
+ rpId: string;
482
+ }): Credential;
483
+ /**
484
+ * Check a `webauthn.get` response from `credential`; its new sign count.
485
+ *
486
+ * `checkCounter: false` is for a verifier without the stored counter (an
487
+ * agent checking an approval offline); the relay always checks it.
488
+ */
489
+ declare function verifyAssertion(credential: Credential, args: {
490
+ authenticatorData: Uint8Array;
491
+ clientDataJSON: Uint8Array;
492
+ signature: Uint8Array;
493
+ challenge: Uint8Array;
494
+ origin: string;
495
+ rpId: string;
496
+ checkCounter?: boolean;
497
+ }): number;
498
+ /**
499
+ * The challenge one Face ID signs to approve `items` (type, hash) as shown.
500
+ *
501
+ * `screen` is the exact text the page showed; the challenge commits to its
502
+ * SHA-256, so the page can't show one thing and get another signed.
503
+ */
504
+ declare function approvalChallenge(items: Array<[string, string]>, screen: string): Uint8Array;
505
+
379
506
  /**
380
507
  * UAM Protocol -- Universal Agent Messaging protocol library.
381
508
  *
@@ -386,6 +513,7 @@ type index_Address = Address;
386
513
  type index_ContactCard = ContactCard;
387
514
  type index_DecryptionError = DecryptionError;
388
515
  declare const index_DecryptionError: typeof DecryptionError;
516
+ type index_DidKeyKind = DidKeyKind;
389
517
  type index_EncryptionError = EncryptionError;
390
518
  declare const index_EncryptionError: typeof EncryptionError;
391
519
  type index_EnvelopeTooLargeError = EnvelopeTooLargeError;
@@ -394,42 +522,58 @@ type index_InvalidAddressError = InvalidAddressError;
394
522
  declare const index_InvalidAddressError: typeof InvalidAddressError;
395
523
  type index_InvalidContactCardError = InvalidContactCardError;
396
524
  declare const index_InvalidContactCardError: typeof InvalidContactCardError;
525
+ type index_InvalidDidKeyError = InvalidDidKeyError;
526
+ declare const index_InvalidDidKeyError: typeof InvalidDidKeyError;
397
527
  type index_InvalidEnvelopeError = InvalidEnvelopeError;
398
528
  declare const index_InvalidEnvelopeError: typeof InvalidEnvelopeError;
399
529
  type index_KeyPinningError = KeyPinningError;
400
530
  declare const index_KeyPinningError: typeof KeyPinningError;
401
531
  type index_Keypair = Keypair;
532
+ declare const index_MAX_ENVELOPE_AGE: typeof MAX_ENVELOPE_AGE;
402
533
  declare const index_MAX_ENVELOPE_SIZE: typeof MAX_ENVELOPE_SIZE;
403
534
  type index_MessageEnvelope = MessageEnvelope;
404
535
  type index_MessageType = MessageType;
405
536
  declare const index_MessageType: typeof MessageType;
406
537
  type index_Seed = Seed;
538
+ type index_SendRefusedError = SendRefusedError;
539
+ declare const index_SendRefusedError: typeof SendRefusedError;
407
540
  type index_SignatureError = SignatureError;
408
541
  declare const index_SignatureError: typeof SignatureError;
409
542
  type index_SignatureVerificationError = SignatureVerificationError;
410
543
  declare const index_SignatureVerificationError: typeof SignatureVerificationError;
544
+ type index_SignedObjectError = SignedObjectError;
545
+ declare const index_SignedObjectError: typeof SignedObjectError;
411
546
  type index_SigningKey = SigningKey;
412
547
  type index_UAMError = UAMError;
413
548
  declare const index_UAMError: typeof UAMError;
414
549
  declare const index_UAM_VERSION: typeof UAM_VERSION;
415
550
  type index_VerifyKey = VerifyKey;
551
+ type index_WebAuthnError = WebAuthnError;
552
+ declare const index_WebAuthnError: typeof WebAuthnError;
553
+ declare const index_approvalChallenge: typeof approvalChallenge;
416
554
  declare const index_b64Decode: typeof b64Decode;
417
555
  declare const index_b64Encode: typeof b64Encode;
556
+ declare const index_canonical: typeof canonical;
418
557
  declare const index_canonicalize: typeof canonicalize;
419
558
  declare const index_contactCardFromDict: typeof contactCardFromDict;
420
559
  declare const index_contactCardToDict: typeof contactCardToDict;
421
560
  declare const index_createContactCard: typeof createContactCard;
422
561
  declare const index_createEnvelope: typeof createEnvelope;
562
+ declare const index_credentialDid: typeof credentialDid;
563
+ declare const index_decodeDidKey: typeof decodeDidKey;
423
564
  declare const index_decryptPayload: typeof decryptPayload;
424
565
  declare const index_decryptPayloadAnonymous: typeof decryptPayloadAnonymous;
425
566
  declare const index_deserializeSigningKey: typeof deserializeSigningKey;
426
567
  declare const index_deserializeVerifyKey: typeof deserializeVerifyKey;
568
+ declare const index_encodeDidKey: typeof encodeDidKey;
427
569
  declare const index_encryptPayload: typeof encryptPayload;
428
570
  declare const index_encryptPayloadAnonymous: typeof encryptPayloadAnonymous;
429
571
  declare const index_fromWireDict: typeof fromWireDict;
430
572
  declare const index_generateKeypair: typeof generateKeypair;
431
573
  declare const index_generateNonce: typeof generateNonce;
574
+ declare const index_objectHash: typeof objectHash;
432
575
  declare const index_parseAddress: typeof parseAddress;
576
+ declare const index_parseBody: typeof parseBody;
433
577
  declare const index_publicKeyFingerprint: typeof publicKeyFingerprint;
434
578
  declare const index_serializeSigningKey: typeof serializeSigningKey;
435
579
  declare const index_serializeVerifyKey: typeof serializeVerifyKey;
@@ -438,11 +582,13 @@ declare const index_sodiumReady: typeof sodiumReady;
438
582
  declare const index_toWireDict: typeof toWireDict;
439
583
  declare const index_utcTimestamp: typeof utcTimestamp;
440
584
  declare const index_validateEnvelopeSize: typeof validateEnvelopeSize;
585
+ declare const index_verifyAssertion: typeof verifyAssertion;
441
586
  declare const index_verifyContactCard: typeof verifyContactCard;
442
587
  declare const index_verifyEnvelope: typeof verifyEnvelope;
588
+ declare const index_verifyRegistration: typeof verifyRegistration;
443
589
  declare const index_verifySignature: typeof verifySignature;
444
590
  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 };
591
+ export { type index_Address as Address, type index_ContactCard as ContactCard, index_DecryptionError as DecryptionError, type index_DidKeyKind as DidKeyKind, index_EncryptionError as EncryptionError, index_EnvelopeTooLargeError as EnvelopeTooLargeError, index_InvalidAddressError as InvalidAddressError, index_InvalidContactCardError as InvalidContactCardError, index_InvalidDidKeyError as InvalidDidKeyError, 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 Credential as PasskeyCredential, type index_Seed as Seed, index_SendRefusedError as SendRefusedError, index_SignatureError as SignatureError, index_SignatureVerificationError as SignatureVerificationError, type Body as SignedBody, index_SignedObjectError as SignedObjectError, type index_SigningKey as SigningKey, index_UAMError as UAMError, index_UAM_VERSION as UAM_VERSION, type index_VerifyKey as VerifyKey, index_WebAuthnError as WebAuthnError, index_approvalChallenge as approvalChallenge, index_b64Decode as b64Decode, index_b64Encode as b64Encode, index_canonical as canonical, index_canonicalize as canonicalize, index_contactCardFromDict as contactCardFromDict, index_contactCardToDict as contactCardToDict, index_createContactCard as createContactCard, index_createEnvelope as createEnvelope, index_credentialDid as credentialDid, index_decodeDidKey as decodeDidKey, index_decryptPayload as decryptPayload, index_decryptPayloadAnonymous as decryptPayloadAnonymous, index_deserializeSigningKey as deserializeSigningKey, index_deserializeVerifyKey as deserializeVerifyKey, index_encodeDidKey as encodeDidKey, index_encryptPayload as encryptPayload, index_encryptPayloadAnonymous as encryptPayloadAnonymous, index_fromWireDict as fromWireDict, index_generateKeypair as generateKeypair, index_generateNonce as generateNonce, index_objectHash as objectHash, index_parseAddress as parseAddress, index_parseBody as parseBody, 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, validate as validateSignedObject, index_verifyAssertion as verifyAssertion, index_verifyContactCard as verifyContactCard, index_verifyEnvelope as verifyEnvelope, index_verifyRegistration as verifyRegistration, index_verifySignature as verifySignature };
446
592
  }
447
593
 
448
594
  /**
@@ -474,13 +620,31 @@ declare class KeyManager {
474
620
  * Store the relay token alongside the keypair.
475
621
  */
476
622
  saveToken(name: string, token: string): void;
623
+ /**
624
+ * Store the address `relayUrl` gave this agent.
625
+ */
626
+ saveAddress(name: string, address: string, relayUrl: string): void;
627
+ /**
628
+ * Load the address saved for `relayUrl`.
629
+ *
630
+ * Returns null if there is none: the file is missing, unreadable or
631
+ * malformed, or it belongs to another relay.
632
+ */
633
+ loadAddress(name: string, relayUrl: string): string | null;
477
634
  /**
478
635
  * Load a previously saved token, or return null.
479
636
  *
480
637
  * Checks UAM_TOKEN env var first, then file-based storage,
481
- * then legacy .api_key files for backward compatibility.
638
+ * then legacy .api_key files for backward compatibility. With `relayUrl`,
639
+ * a saved token that belongs to another relay counts as none, so it is
640
+ * only offered to the relay that issued it.
641
+ */
642
+ loadToken(name: string, relayUrl?: string): string | null;
643
+ /**
644
+ * True if the saved address says this agent's files are another relay's.
645
+ * A missing or damaged file says nothing.
482
646
  */
483
- loadToken(name: string): string | null;
647
+ private _savedForAnotherRelay;
484
648
  /** Set file permissions to 0o600 (owner read/write only). */
485
649
  private _setPermissions;
486
650
  /** Warn if key file permissions are too permissive. */
@@ -512,6 +676,30 @@ interface ReceivedMessage {
512
676
  readonly mediaType?: string;
513
677
  readonly verified: boolean;
514
678
  }
679
+ /** What a relay can say it did with a sent message (as the Python SDK). */
680
+ declare const SEND_STATUSES: readonly ["delivered", "stored", "relayed", "queued", "duplicate", "dropped", "pending", "accepted", "unknown"];
681
+ type SendStatus = (typeof SEND_STATUSES)[number];
682
+ /**
683
+ * What Agent.send() returns.
684
+ *
685
+ * `status` is one of SEND_STATUSES: "pending" means the recipient only
686
+ * takes messages from approved contacts and the relay kept nothing.
687
+ * `delivered` is true only when the relay says the recipient has it.
688
+ */
689
+ interface SendResult {
690
+ readonly messageId: string;
691
+ readonly status: SendStatus;
692
+ readonly delivered: boolean;
693
+ }
694
+ /**
695
+ * True when a relay's features include `name` at `version` or later.
696
+ *
697
+ * "name" is version 1; "name:N" means version N or later.
698
+ */
699
+ declare function supports(features: Iterable<string>, name: string, version?: number): boolean;
700
+ /** What to tell whoever sent a message the relay answered "pending". */
701
+ declare const PENDING_NOTE: string;
702
+ declare function pendingNote(to: string): string;
515
703
 
516
704
  /**
517
705
  * SQLite-backed local contact storage (HAND-03).
@@ -583,6 +771,14 @@ declare class ContactBook {
583
771
  getRelayUrls(address: string): string[] | null;
584
772
  /**
585
773
  * Add or update a contact (upsert).
774
+ *
775
+ * Refuses to replace the key of a contact whose trust_state is "pinned"
776
+ * or "verified" with a different key, unless `force: true` is passed (for
777
+ * a deliberate key rotation, which is logged). Matches the Python SDK's
778
+ * ContactBook.add_contact.
779
+ *
780
+ * @throws {KeyPinningError} If a pinned/verified contact's key would be
781
+ * replaced by a different key without `force`.
586
782
  */
587
783
  addContact(address: string, publicKey: string, options?: {
588
784
  displayName?: string | null;
@@ -590,6 +786,7 @@ declare class ContactBook {
590
786
  trustSource?: string | null;
591
787
  relay?: string | null;
592
788
  relays?: string[] | null;
789
+ force?: boolean;
593
790
  }): void;
594
791
  /**
595
792
  * Return all contacts sorted by last_seen DESC.
@@ -666,7 +863,7 @@ interface AgentLike {
666
863
  readonly signingKey: Uint8Array;
667
864
  };
668
865
  readonly _transport: {
669
- send(envelope: Record<string, unknown>): Promise<void>;
866
+ send(envelope: Record<string, unknown>): Promise<unknown>;
670
867
  } | null;
671
868
  }
672
869
  declare class HandshakeManager {
@@ -694,6 +891,16 @@ declare class HandshakeManager {
694
891
  private _handleRequest;
695
892
  /**
696
893
  * Process a handshake.accept: store the sender as pinned (TOFU).
894
+ *
895
+ * Only honoured when:
896
+ * 1. we previously sent this address a handshake.request
897
+ * (trust_state === "handshake-sent"); anything else is an unsolicited
898
+ * accept and is dropped. This also means an accept can never
899
+ * downgrade a trusted/verified contact to pinned;
900
+ * 2. the payload decrypts with our key and the sender's key, and parses
901
+ * as JSON. Otherwise it is dropped without pinning;
902
+ * 3. an embedded contact_card, if present, is well-formed, correctly
903
+ * self-signed, and bound to the envelope sender (throws otherwise).
697
904
  */
698
905
  private _handleAccept;
699
906
  /**
@@ -718,8 +925,13 @@ declare abstract class TransportBase {
718
925
  abstract connect(): Promise<void>;
719
926
  /** Close the connection. */
720
927
  abstract disconnect(): Promise<void>;
721
- /** Send a message envelope to the relay. */
722
- abstract send(envelope: Record<string, unknown>): Promise<void>;
928
+ /**
929
+ * Send a message envelope to the relay.
930
+ *
931
+ * Resolves with the relay's answer (`{status, ...}`), or null when none
932
+ * came in time: the message may still have arrived.
933
+ */
934
+ abstract send(envelope: Record<string, unknown>): Promise<Record<string, unknown> | null>;
723
935
  /** Retrieve pending messages from the relay. */
724
936
  abstract receive(limit?: number): Promise<Record<string, unknown>[]>;
725
937
  /**
@@ -731,6 +943,55 @@ declare abstract class TransportBase {
731
943
  abstract listen(callback: (msg: Record<string, unknown>) => Promise<void>): Promise<void>;
732
944
  }
733
945
 
946
+ /**
947
+ * Inbound (from address, message id) pairs kept in a SQLite file.
948
+ *
949
+ * Opens itself on first use, so an Agent driven without connect() still has
950
+ * replay protection. Safe to share between processes: the insert is the
951
+ * check (INSERT OR IGNORE), in WAL mode.
952
+ */
953
+ declare class PersistentReplayCache {
954
+ readonly capacity: number;
955
+ private readonly _path;
956
+ private readonly _now;
957
+ private _db;
958
+ private _opening;
959
+ private _count;
960
+ private _lastPurge;
961
+ private _createdAt;
962
+ private readonly _overflow;
963
+ /**
964
+ * @param path - The SQLite file
965
+ * @param options.now - The clock, in ms since the epoch
966
+ */
967
+ constructor(path: string, options?: {
968
+ capacity?: number;
969
+ now?: () => number;
970
+ });
971
+ /** Open the file now, rather than on first use. */
972
+ open(): Promise<void>;
973
+ /**
974
+ * Return true if the envelope must be dropped as a replay. Otherwise record
975
+ * the pair and return false.
976
+ *
977
+ * @param signedAt - The envelope's signed time, in ms since the epoch. It
978
+ * matters only when the cache is full.
979
+ */
980
+ seenOrRecord(fromAddress: string, messageId: string, signedAt: number): Promise<boolean>;
981
+ /** When this cache file was first created, in ms since the epoch. */
982
+ createdAt(): Promise<number>;
983
+ /** True if an envelope signed at `signedAt` (ms) predates this cache. */
984
+ signedBeforeCache(signedAt: number): Promise<boolean>;
985
+ /** Number of ids currently held on disk. */
986
+ count(): Promise<number>;
987
+ /** Close the file; the next call opens it again. */
988
+ close(): Promise<void>;
989
+ private _open;
990
+ private _connect;
991
+ private _purge;
992
+ private _countRows;
993
+ }
994
+
734
995
  /**
735
996
  * Agent class -- the primary SDK interface (SDK-01).
736
997
  *
@@ -756,6 +1017,23 @@ declare class Agent {
756
1017
  readonly _contactBook: ContactBook;
757
1018
  /** @internal */
758
1019
  readonly _handshake: HandshakeManager;
1020
+ /**
1021
+ * @internal Wall clock in ms since the epoch, used by the inbound
1022
+ * freshness/expiry checks and the replay cache. Replaceable in tests.
1023
+ */
1024
+ _clock: () => number;
1025
+ /**
1026
+ * @internal Inbound replay cache, keyed by (from address, message id).
1027
+ * Kept on disk under the data dir, so it outlives restarts and is shared by
1028
+ * every process using it; capacity from UAM_REPLAY_CACHE_CAPACITY.
1029
+ */
1030
+ readonly _replayCache: PersistentReplayCache;
1031
+ /** Whether our relay has said it writes relay_meta. */
1032
+ private _relayStamps;
1033
+ /** Until when, by _monotonic, to take the relay's last no as still true. */
1034
+ private _relayNoUntil;
1035
+ /** @internal Monotonic clock in ms. Replaceable in tests. */
1036
+ _monotonic: () => number;
759
1037
  /**
760
1038
  * Create an Agent. No I/O happens here -- call connect() to initialize.
761
1039
  */
@@ -791,17 +1069,25 @@ declare class Agent {
791
1069
  */
792
1070
  close(): Promise<void>;
793
1071
  /**
794
- * Send an encrypted, signed message. Returns the message_id.
1072
+ * Send an encrypted, signed message.
1073
+ *
1074
+ * Returns the message id with what the relay did with it: `status`
1075
+ * "pending" means the recipient only takes approved contacts, and the
1076
+ * message was not kept (don't resend it). `delivered` is true only when
1077
+ * the relay says the recipient has it.
795
1078
  */
796
1079
  send(toAddress: string, message: string, options?: {
797
1080
  threadId?: string;
798
1081
  attachments?: Array<Record<string, unknown>>;
799
- }): Promise<string>;
1082
+ }): Promise<SendResult>;
800
1083
  /**
801
1084
  * Retrieve, decrypt, and verify pending messages.
802
1085
  *
803
1086
  * Returns a list of ReceivedMessage data objects.
804
- * Silently drops messages with invalid signatures or decryption errors.
1087
+ * Drops messages with invalid signatures or decryption errors. A message
1088
+ * that fails processing for any other reason (malformed envelope, bad
1089
+ * handshake payload, ...) is logged and dropped; it never aborts the
1090
+ * rest of the batch.
805
1091
  */
806
1092
  inbox(limit?: number): Promise<ReceivedMessage[]>;
807
1093
  /** List pending handshake requests. */
@@ -857,6 +1143,10 @@ declare class Agent {
857
1143
  /**
858
1144
  * Resolve a recipient's public key, checking contact book first.
859
1145
  *
1146
+ * Pure lookup: never writes to the contact book. The contact row is
1147
+ * created by _initiateHandshake (as "handshake-sent") once the request
1148
+ * has gone out, giving the lifecycle no row -> handshake-sent -> pinned.
1149
+ *
860
1150
  * TOFU-01: Known contacts always use stored key (zero network I/O).
861
1151
  * TOFU-03: Race-condition double-check after network resolve.
862
1152
  * Raises KeyPinningError if a pinned contact's key doesn't match.
@@ -869,6 +1159,26 @@ declare class Agent {
869
1159
  * Returns null for invalid messages or handshake protocol messages.
870
1160
  */
871
1161
  private _processInbound;
1162
+ /**
1163
+ * Recipient, freshness, expiry and replay checks for a signature-verified
1164
+ * inbound envelope. Returns false (and logs) when it must be dropped.
1165
+ * Matches the Python SDK's _process_inbound checks.
1166
+ */
1167
+ private _passesInboundChecks;
1168
+ /** True if a message too old to be live is stored mail, not a replay. */
1169
+ private _wasStored;
1170
+ /**
1171
+ * True if our relay writes relay_meta, so a sender can't have.
1172
+ *
1173
+ * Asked only when a stale message carries a stamp. A yes is remembered for
1174
+ * good, a no for RELAY_NO_MEMORY_MS: a relay can be upgraded while this
1175
+ * agent runs. A failed lookup isn't remembered, because the relay has
1176
+ * already marked this mail delivered and the next message should get a
1177
+ * real answer.
1178
+ */
1179
+ private _relayStampsMail;
1180
+ /** The relay's features; a body that doesn't list any is an empty list. */
1181
+ private _fetchRelayFeatures;
872
1182
  /**
873
1183
  * Sweep expired pending handshakes and send receipt.failed (HAND-03).
874
1184
  */
@@ -883,10 +1193,80 @@ declare class Agent {
883
1193
  /**
884
1194
  * Register with the relay server.
885
1195
  *
886
- * Calls POST /api/v1/register with agent name and public key.
887
- * Stores the returned API key on disk.
1196
+ * Calls POST /api/v1/register with agent name and public key. If the
1197
+ * address exists (409), repeats it with a proof that this agent holds the
1198
+ * key. Stores the returned API key on disk.
888
1199
  */
889
1200
  private _registerWithRelay;
1201
+ /** This agent's address on the relay, for signing a proof. */
1202
+ private _addressOnRelay;
1203
+ /**
1204
+ * True if the relay's URL fits `domain`, so a proof can be signed for it.
1205
+ *
1206
+ * A proof names an address on one relay, so it is only signed for a domain
1207
+ * this URL belongs to. A relay on this machine may use any domain.
1208
+ */
1209
+ private _relayMayUse;
1210
+ private _postRegistration;
1211
+ /**
1212
+ * Sign a statement that this agent holds the key for `address`.
1213
+ *
1214
+ * The signed string must stay byte-identical to the Python SDK's.
1215
+ */
1216
+ private _registrationProof;
1217
+ private _ownershipNotProven;
1218
+ /** Remember `address` for this relay, if the key folder allows it. */
1219
+ private _saveAddress;
1220
+ /** Saving is best effort: a key folder we can't write doesn't stop us. */
1221
+ private _tryToSave;
1222
+ /** Take `address` as ours; its domain is this relay's real domain. */
1223
+ private _useAddress;
1224
+ /**
1225
+ * The address the stored token belongs to.
1226
+ *
1227
+ * An explicit domain settles it. Otherwise the relay chooses the domain,
1228
+ * and it need not be the relay's hostname: use the address saved for this
1229
+ * relay, or ask the relay once and keep the answer.
1230
+ */
1231
+ private _storedAddress;
1232
+ /** Ask the relay for its domain; null if it cannot say. */
1233
+ private _fetchRelayDomain;
890
1234
  }
891
1235
 
892
- export { Agent, type ReceivedMessage, index as protocol };
1236
+ /**
1237
+ * Receiver-side webhook signature verification (HOOK-03).
1238
+ *
1239
+ * Small utility for webhook receivers to verify that incoming webhook
1240
+ * payloads were signed by the UAM relay. Uses HMAC-SHA256 with the
1241
+ * agent's webhook secret, which the relay returns when the webhook is set
1242
+ * (PUT /api/v1/agents/{address}/webhook). Setting the same URL again
1243
+ * returns it again.
1244
+ *
1245
+ * Matches Python webhook_verify.py API surface.
1246
+ *
1247
+ * Usage:
1248
+ *
1249
+ * import { verifyWebhookSignature } from "@youam/sdk";
1250
+ *
1251
+ * const payload = Buffer.from(requestBody);
1252
+ * const signature = request.headers["x-uam-signature"];
1253
+ * const secret = "your-webhook-secret";
1254
+ *
1255
+ * if (verifyWebhookSignature(payload, signature, secret)) {
1256
+ * // payload is authentic
1257
+ * }
1258
+ */
1259
+ /**
1260
+ * Verify an HMAC-SHA256 webhook signature.
1261
+ *
1262
+ * @param payload - Raw request body bytes
1263
+ * @param signatureHeader - "sha256=<hex>" format (X-UAM-Signature header)
1264
+ * @param secret - The agent's webhook secret
1265
+ * @returns `true` if valid, `false` otherwise.
1266
+ *
1267
+ * Uses `crypto.timingSafeEqual()` for constant-time comparison
1268
+ * to prevent timing attacks.
1269
+ */
1270
+ declare function verifyWebhookSignature(payload: Buffer | Uint8Array, signatureHeader: string, secret: string): boolean;
1271
+
1272
+ export { Agent, ContactBook, PENDING_NOTE, type ReceivedMessage, SEND_STATUSES, type SendResult, type SendStatus, pendingNote, index as protocol, supports, verifyWebhookSignature };