@t402/extensions 2.0.0 → 2.3.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/cjs/index.js CHANGED
@@ -31,15 +31,29 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  BAZAAR: () => BAZAAR,
34
+ SIWX_EXTENSION_KEY: () => SIWX_EXTENSION_KEY,
35
+ SIWX_HEADER_NAME: () => SIWX_HEADER_NAME,
34
36
  bazaarResourceServerExtension: () => bazaarResourceServerExtension,
37
+ constructMessage: () => constructMessage,
38
+ createSIWxMessage: () => createSIWxMessage,
39
+ createSIWxPayload: () => createSIWxPayload,
40
+ createSIWxTypedData: () => createSIWxTypedData,
35
41
  declareDiscoveryExtension: () => declareDiscoveryExtension,
42
+ declareSIWxExtension: () => declareSIWxExtension,
43
+ encodeSIWxHeader: () => encodeSIWxHeader,
36
44
  extractDiscoveryInfo: () => extractDiscoveryInfo,
37
45
  extractDiscoveryInfoFromExtension: () => extractDiscoveryInfoFromExtension,
38
46
  extractDiscoveryInfoV1: () => extractDiscoveryInfoV1,
39
47
  extractResourceMetadataV1: () => extractResourceMetadataV1,
48
+ hashMessage: () => hashMessage,
40
49
  isDiscoverableV1: () => isDiscoverableV1,
50
+ parseSIWxHeader: () => parseSIWxHeader,
51
+ signSIWxMessage: () => signSIWxMessage,
41
52
  validateAndExtract: () => validateAndExtract,
42
53
  validateDiscoveryExtension: () => validateDiscoveryExtension,
54
+ validateSIWxMessage: () => validateSIWxMessage,
55
+ verifyEIP6492Signature: () => verifyEIP6492Signature,
56
+ verifySIWxSignature: () => verifySIWxSignature,
43
57
  withBazaar: () => withBazaar
44
58
  });
45
59
  module.exports = __toCommonJS(src_exports);
@@ -474,18 +488,428 @@ function withBazaar(client) {
474
488
  };
475
489
  return extended;
476
490
  }
491
+
492
+ // src/sign-in-with-x/server.ts
493
+ var import_crypto = require("crypto");
494
+ var import_sha3 = require("@noble/hashes/sha3");
495
+ var import_secp256k1 = require("@noble/curves/secp256k1");
496
+ var import_utils = require("@noble/hashes/utils");
497
+ var SIWX_SCHEMA = {
498
+ type: "object",
499
+ required: ["domain", "address", "uri", "version", "chainId", "nonce", "issuedAt", "signature"],
500
+ properties: {
501
+ domain: { type: "string" },
502
+ address: { type: "string" },
503
+ statement: { type: "string" },
504
+ uri: { type: "string" },
505
+ version: { type: "string" },
506
+ chainId: { type: "string" },
507
+ nonce: { type: "string" },
508
+ issuedAt: { type: "string", format: "date-time" },
509
+ expirationTime: { type: "string", format: "date-time" },
510
+ notBefore: { type: "string", format: "date-time" },
511
+ requestId: { type: "string" },
512
+ resources: { type: "array", items: { type: "string" } },
513
+ signature: { type: "string" }
514
+ }
515
+ };
516
+ var EIP1271_MAGIC_VALUE = "0x1626ba7e";
517
+ function extractDomain(resourceUri) {
518
+ try {
519
+ const url = new URL(resourceUri);
520
+ return url.host;
521
+ } catch {
522
+ return resourceUri.replace(/^https?:\/\//, "").split("/")[0];
523
+ }
524
+ }
525
+ function generateNonce() {
526
+ return (0, import_crypto.randomBytes)(16).toString("hex");
527
+ }
528
+ function declareSIWxExtension(options) {
529
+ const domain = extractDomain(options.resourceUri);
530
+ const now = /* @__PURE__ */ new Date();
531
+ const expirationTime = options.expirationTime || new Date(now.getTime() + 5 * 60 * 1e3).toISOString();
532
+ const info = {
533
+ domain,
534
+ uri: options.resourceUri,
535
+ statement: options.statement,
536
+ version: options.version || "1",
537
+ chainId: options.network,
538
+ nonce: generateNonce(),
539
+ issuedAt: now.toISOString(),
540
+ expirationTime,
541
+ resources: [options.resourceUri],
542
+ signatureScheme: options.signatureScheme
543
+ };
544
+ return {
545
+ info,
546
+ schema: SIWX_SCHEMA
547
+ };
548
+ }
549
+ function parseSIWxHeader(header) {
550
+ if (!header) {
551
+ throw new Error("Missing SIWx header");
552
+ }
553
+ try {
554
+ const decoded = Buffer.from(header, "base64").toString("utf-8");
555
+ const payload = JSON.parse(decoded);
556
+ const required = [
557
+ "domain",
558
+ "address",
559
+ "uri",
560
+ "version",
561
+ "chainId",
562
+ "nonce",
563
+ "issuedAt",
564
+ "signature"
565
+ ];
566
+ for (const field of required) {
567
+ if (!(field in payload)) {
568
+ throw new Error(`Missing required field: ${field}`);
569
+ }
570
+ }
571
+ return payload;
572
+ } catch (error) {
573
+ if (error instanceof SyntaxError) {
574
+ throw new Error("Invalid SIWx header: malformed JSON");
575
+ }
576
+ throw error;
577
+ }
578
+ }
579
+ function validateSIWxMessage(message, expectedResourceUri, options = {}) {
580
+ const { maxAge = 5 * 60 * 1e3, checkNonce } = options;
581
+ const expectedDomain = extractDomain(expectedResourceUri);
582
+ if (message.domain !== expectedDomain) {
583
+ return {
584
+ valid: false,
585
+ error: `Domain mismatch: expected ${expectedDomain}, got ${message.domain}`
586
+ };
587
+ }
588
+ if (message.uri !== expectedResourceUri) {
589
+ return {
590
+ valid: false,
591
+ error: `URI mismatch: expected ${expectedResourceUri}, got ${message.uri}`
592
+ };
593
+ }
594
+ if (message.version !== "1") {
595
+ return { valid: false, error: `Unsupported version: ${message.version}` };
596
+ }
597
+ const issuedAt = new Date(message.issuedAt);
598
+ const now = /* @__PURE__ */ new Date();
599
+ if (now.getTime() - issuedAt.getTime() > maxAge) {
600
+ return { valid: false, error: "Message has expired (issuedAt too old)" };
601
+ }
602
+ if (message.expirationTime) {
603
+ const expiration = new Date(message.expirationTime);
604
+ if (expiration < now) {
605
+ return { valid: false, error: "Message has expired" };
606
+ }
607
+ }
608
+ if (message.notBefore) {
609
+ const notBefore = new Date(message.notBefore);
610
+ if (notBefore > now) {
611
+ return { valid: false, error: "Message not yet valid (notBefore in future)" };
612
+ }
613
+ }
614
+ if (checkNonce && !checkNonce(message.nonce)) {
615
+ return { valid: false, error: "Invalid nonce (replay attack detected)" };
616
+ }
617
+ return { valid: true };
618
+ }
619
+ async function verifySIWxSignature(message, signature, options = {}) {
620
+ const { checkSmartWallet = false } = options;
621
+ try {
622
+ const messageText = constructMessage(message);
623
+ const messageHash = hashMessage(messageText);
624
+ const recoveredAddress = recoverAddress(messageHash, signature);
625
+ if (recoveredAddress.toLowerCase() !== message.address.toLowerCase()) {
626
+ if (checkSmartWallet && options.provider) {
627
+ const isValidSmartWallet = await verifySmartWalletSignature(
628
+ message.address,
629
+ messageHash,
630
+ signature,
631
+ options.provider
632
+ );
633
+ if (isValidSmartWallet) {
634
+ return { valid: true, address: message.address };
635
+ }
636
+ }
637
+ return {
638
+ valid: false,
639
+ address: recoveredAddress,
640
+ error: `Signature mismatch: expected ${message.address}, recovered ${recoveredAddress}`
641
+ };
642
+ }
643
+ return { valid: true, address: recoveredAddress };
644
+ } catch (error) {
645
+ return {
646
+ valid: false,
647
+ error: `Signature verification failed: ${error instanceof Error ? error.message : "unknown error"}`
648
+ };
649
+ }
650
+ }
651
+ function constructMessage(payload) {
652
+ const lines = [];
653
+ lines.push(`${payload.domain} wants you to sign in with your ${payload.chainId} account:`);
654
+ lines.push(payload.address);
655
+ lines.push("");
656
+ if (payload.statement) {
657
+ lines.push(payload.statement);
658
+ lines.push("");
659
+ }
660
+ lines.push(`URI: ${payload.uri}`);
661
+ lines.push(`Version: ${payload.version}`);
662
+ lines.push(`Chain ID: ${payload.chainId}`);
663
+ lines.push(`Nonce: ${payload.nonce}`);
664
+ lines.push(`Issued At: ${payload.issuedAt}`);
665
+ if (payload.expirationTime) {
666
+ lines.push(`Expiration Time: ${payload.expirationTime}`);
667
+ }
668
+ if (payload.notBefore) {
669
+ lines.push(`Not Before: ${payload.notBefore}`);
670
+ }
671
+ if (payload.requestId) {
672
+ lines.push(`Request ID: ${payload.requestId}`);
673
+ }
674
+ if (payload.resources && payload.resources.length > 0) {
675
+ lines.push("Resources:");
676
+ for (const resource of payload.resources) {
677
+ lines.push(`- ${resource}`);
678
+ }
679
+ }
680
+ return lines.join("\n");
681
+ }
682
+ function hashMessage(message) {
683
+ const messageBytes = new TextEncoder().encode(message);
684
+ const prefix = `Ethereum Signed Message:
685
+ ${messageBytes.length}`;
686
+ const prefixBytes = new TextEncoder().encode(prefix);
687
+ const combined = new Uint8Array(prefixBytes.length + messageBytes.length);
688
+ combined.set(prefixBytes, 0);
689
+ combined.set(messageBytes, prefixBytes.length);
690
+ const hash = (0, import_sha3.keccak_256)(combined);
691
+ return "0x" + (0, import_utils.bytesToHex)(hash);
692
+ }
693
+ function recoverAddress(messageHash, signature) {
694
+ const sigHex = signature.startsWith("0x") ? signature.slice(2) : signature;
695
+ const hashHex = messageHash.startsWith("0x") ? messageHash.slice(2) : messageHash;
696
+ if (sigHex.length !== 130) {
697
+ throw new Error(`Invalid signature length: expected 130 hex chars, got ${sigHex.length}`);
698
+ }
699
+ const r = BigInt("0x" + sigHex.slice(0, 64));
700
+ const s = BigInt("0x" + sigHex.slice(64, 128));
701
+ let v = parseInt(sigHex.slice(128, 130), 16);
702
+ if (v >= 27) {
703
+ v -= 27;
704
+ }
705
+ if (v !== 0 && v !== 1) {
706
+ throw new Error(`Invalid recovery id: ${v}`);
707
+ }
708
+ const sig = new import_secp256k1.secp256k1.Signature(r, s).addRecoveryBit(v);
709
+ const hashBytes = (0, import_utils.hexToBytes)(hashHex);
710
+ const publicKey = sig.recoverPublicKey(hashBytes);
711
+ const pubKeyBytes = publicKey.toRawBytes(false).slice(1);
712
+ const addressHash = (0, import_sha3.keccak_256)(pubKeyBytes);
713
+ const addressBytes = addressHash.slice(-20);
714
+ const address = "0x" + (0, import_utils.bytesToHex)(addressBytes);
715
+ return toChecksumAddress(address);
716
+ }
717
+ function toChecksumAddress(address) {
718
+ const addr = address.toLowerCase().replace("0x", "");
719
+ const hash = (0, import_utils.bytesToHex)((0, import_sha3.keccak_256)(new TextEncoder().encode(addr)));
720
+ let checksummed = "0x";
721
+ for (let i = 0; i < 40; i++) {
722
+ if (parseInt(hash[i], 16) >= 8) {
723
+ checksummed += addr[i].toUpperCase();
724
+ } else {
725
+ checksummed += addr[i];
726
+ }
727
+ }
728
+ return checksummed;
729
+ }
730
+ async function verifySmartWalletSignature(walletAddress, messageHash, signature, provider) {
731
+ function isEthProvider(p) {
732
+ return typeof p === "object" && p !== null && "request" in p;
733
+ }
734
+ if (!isEthProvider(provider)) {
735
+ return false;
736
+ }
737
+ try {
738
+ const hashHex = messageHash.startsWith("0x") ? messageHash : "0x" + messageHash;
739
+ const sigHex = signature.startsWith("0x") ? signature : "0x" + signature;
740
+ const data = "0x1626ba7e" + hashHex.slice(2).padStart(64, "0") + // bytes32 hash
741
+ "0000000000000000000000000000000000000000000000000000000000000040" + // offset to bytes
742
+ (sigHex.length / 2 - 1).toString(16).padStart(64, "0") + // bytes length
743
+ sigHex.slice(2).padEnd(Math.ceil((sigHex.length - 2) / 64) * 64, "0");
744
+ const result = await provider.request({
745
+ method: "eth_call",
746
+ params: [{ to: walletAddress, data }, "latest"]
747
+ });
748
+ return result.toLowerCase().startsWith(EIP1271_MAGIC_VALUE);
749
+ } catch {
750
+ return false;
751
+ }
752
+ }
753
+ async function verifyEIP6492Signature(walletAddress, messageHash, signature, provider) {
754
+ const EIP6492_SUFFIX = "6492649264926492649264926492649264926492649264926492649264926492";
755
+ const sigHex = signature.startsWith("0x") ? signature.slice(2) : signature;
756
+ if (sigHex.endsWith(EIP6492_SUFFIX)) {
757
+ console.warn("EIP-6492 deployment signatures not yet fully implemented");
758
+ return false;
759
+ }
760
+ return verifySmartWalletSignature(walletAddress, messageHash, signature, provider);
761
+ }
762
+
763
+ // src/sign-in-with-x/client.ts
764
+ function encodeSIWxHeader(payload) {
765
+ const json = JSON.stringify(payload);
766
+ if (typeof Buffer !== "undefined") {
767
+ return Buffer.from(json, "utf-8").toString("base64");
768
+ }
769
+ return btoa(json);
770
+ }
771
+ function createSIWxMessage(serverInfo, address) {
772
+ const payload = {
773
+ domain: serverInfo.domain,
774
+ address,
775
+ statement: serverInfo.statement,
776
+ uri: serverInfo.uri,
777
+ version: serverInfo.version,
778
+ chainId: serverInfo.chainId,
779
+ nonce: serverInfo.nonce,
780
+ issuedAt: serverInfo.issuedAt,
781
+ expirationTime: serverInfo.expirationTime,
782
+ notBefore: serverInfo.notBefore,
783
+ requestId: serverInfo.requestId,
784
+ resources: serverInfo.resources,
785
+ signature: ""
786
+ // Will be added after signing
787
+ };
788
+ return constructMessage(payload);
789
+ }
790
+ function createSIWxTypedData(serverInfo, address) {
791
+ const chainIdParts = serverInfo.chainId.split(":");
792
+ const chainIdNum = chainIdParts.length > 1 ? parseInt(chainIdParts[1], 10) : parseInt(chainIdParts[0], 10);
793
+ return {
794
+ domain: {
795
+ name: serverInfo.domain,
796
+ version: serverInfo.version,
797
+ chainId: chainIdNum
798
+ },
799
+ types: {
800
+ SIWx: [
801
+ { name: "domain", type: "string" },
802
+ { name: "address", type: "address" },
803
+ { name: "statement", type: "string" },
804
+ { name: "uri", type: "string" },
805
+ { name: "version", type: "string" },
806
+ { name: "chainId", type: "string" },
807
+ { name: "nonce", type: "string" },
808
+ { name: "issuedAt", type: "string" },
809
+ { name: "expirationTime", type: "string" },
810
+ { name: "resources", type: "string[]" }
811
+ ]
812
+ },
813
+ primaryType: "SIWx",
814
+ message: {
815
+ domain: serverInfo.domain,
816
+ address,
817
+ statement: serverInfo.statement || "",
818
+ uri: serverInfo.uri,
819
+ version: serverInfo.version,
820
+ chainId: serverInfo.chainId,
821
+ nonce: serverInfo.nonce,
822
+ issuedAt: serverInfo.issuedAt,
823
+ expirationTime: serverInfo.expirationTime || "",
824
+ resources: serverInfo.resources || []
825
+ }
826
+ };
827
+ }
828
+ async function signSIWxMessage(message, signer, options) {
829
+ const scheme = options?.signatureScheme || "eip191";
830
+ switch (scheme) {
831
+ case "eip191":
832
+ if (!signer.signMessage) {
833
+ throw new Error("Signer does not support personal_sign (EIP-191)");
834
+ }
835
+ return signer.signMessage(message);
836
+ case "eip712":
837
+ if (!signer.signTypedData) {
838
+ throw new Error("Signer does not support signTypedData (EIP-712)");
839
+ }
840
+ if (!options?.serverInfo) {
841
+ throw new Error("EIP-712 signing requires serverInfo in options");
842
+ }
843
+ const typedData = createSIWxTypedData(options.serverInfo, signer.address);
844
+ return signer.signTypedData(typedData);
845
+ case "eip1271":
846
+ case "eip6492":
847
+ if (!signer.signMessage) {
848
+ throw new Error("Signer does not support signing");
849
+ }
850
+ return signer.signMessage(message);
851
+ case "siws":
852
+ if (!signer.signMessage) {
853
+ throw new Error("Signer does not support signing");
854
+ }
855
+ return signer.signMessage(message);
856
+ case "sep10":
857
+ throw new Error("Stellar SEP-10 signing not yet implemented");
858
+ default:
859
+ throw new Error(`Unknown signature scheme: ${scheme}`);
860
+ }
861
+ }
862
+ async function createSIWxPayload(serverExtension, signer) {
863
+ const { info } = serverExtension;
864
+ const message = createSIWxMessage(info, signer.address);
865
+ const signature = await signSIWxMessage(message, signer, {
866
+ signatureScheme: info.signatureScheme,
867
+ serverInfo: info
868
+ });
869
+ return {
870
+ domain: info.domain,
871
+ address: signer.address,
872
+ statement: info.statement,
873
+ uri: info.uri,
874
+ version: info.version,
875
+ chainId: info.chainId,
876
+ nonce: info.nonce,
877
+ issuedAt: info.issuedAt,
878
+ expirationTime: info.expirationTime,
879
+ notBefore: info.notBefore,
880
+ requestId: info.requestId,
881
+ resources: info.resources,
882
+ signature
883
+ };
884
+ }
885
+ var SIWX_EXTENSION_KEY = "siwx";
886
+ var SIWX_HEADER_NAME = "X-T402-SIWx";
477
887
  // Annotate the CommonJS export names for ESM import in node:
478
888
  0 && (module.exports = {
479
889
  BAZAAR,
890
+ SIWX_EXTENSION_KEY,
891
+ SIWX_HEADER_NAME,
480
892
  bazaarResourceServerExtension,
893
+ constructMessage,
894
+ createSIWxMessage,
895
+ createSIWxPayload,
896
+ createSIWxTypedData,
481
897
  declareDiscoveryExtension,
898
+ declareSIWxExtension,
899
+ encodeSIWxHeader,
482
900
  extractDiscoveryInfo,
483
901
  extractDiscoveryInfoFromExtension,
484
902
  extractDiscoveryInfoV1,
485
903
  extractResourceMetadataV1,
904
+ hashMessage,
486
905
  isDiscoverableV1,
906
+ parseSIWxHeader,
907
+ signSIWxMessage,
487
908
  validateAndExtract,
488
909
  validateDiscoveryExtension,
910
+ validateSIWxMessage,
911
+ verifyEIP6492Signature,
912
+ verifySIWxSignature,
489
913
  withBazaar
490
914
  });
491
915
  //# sourceMappingURL=index.js.map