Notarize AT Protocol records on Ethereum using EAS (experiment)

Compare changes

Choose any two refs to compare.

+2 -1
.gitignore
··· 3 3 node_modules 4 4 bun.lock 5 5 dist 6 - coverage 6 + coverage 7 + .atnotary*
+14 -26
README.md
··· 4 4 5 5 ## Why 6 6 7 - **The Problem:** On current social media, anyone can make statements and delete them later without accountability. There's no proof something was said, leading to endless "he said/she said" disputes. Bad actors can repeat harmful patterns because evidence disappears. 8 - 9 - **The Solution:** Create permanent, cryptographically verifiable attestations using EAS (Ethereum Attestation Service). Not to police speech, but to preserve a transparent historical record. Like how Git preserves commit history or Bitcoin preserves transactions - complete transparency creates natural accountability. 10 - 11 - **Why EAS:** 12 7 - **Standardized attestations** - interoperable format used across ecosystem 13 8 - **On-chain verification** - stored on Ethereum/Base, immutable and public 14 9 - **Composable** - other projects can reference and build on attestations 15 10 - **Infrastructure** - explorers, indexers, and tooling already exist 16 11 - **Multi-chain** - works on Ethereum, Base, Optimism, Arbitrum 17 12 18 - **Why AT Protocol:** 19 - Unlike Twitter, AT Protocol is federated and open. Users own their data, can switch servers, and the protocol is public. This makes it possible to build open infrastructure for accountability that isn't controlled by any company. 20 - 21 - **Principles:** 22 - - Transparency without judgment - just preserves facts, doesn't label "good" or "bad" 23 - - Decentralized - fetches directly from user's PDS, not centralized APIs 24 - - User choice - opt-in system, you choose what to notarize 25 - - Open infrastructure - anyone can verify, build on, or run their own instance 26 - 27 - Think of it as combining two open protocols: **AT Protocol** (decentralized social) + **EAS** (decentralized attestations) = transparent, accountable social web. 28 - 29 13 ## What Gets Attested 30 14 31 15 - `recordURI` - Full AT Protocol URI 32 16 - `cid` - AT Protocol's content identifier 33 - - `contentHash` - SHA-256 hash 17 + - `contentHash` - DAG-CBOR hash 34 18 - `pds` - Personal Data Server URL 35 19 - `timestamp` - When attested 36 20 ··· 42 26 43 27 ## Setup 44 28 45 - 1. **Create `.env` file:** 29 + 1. **Create config file:** 46 30 47 31 ```bash 48 - PRIVATE_KEY="0x..." 49 - # SCHEMA_UID is optional - default schemas are provided 32 + atnotary config 50 33 ``` 51 34 52 - 2. **Get testnet ETH:** 35 + 2. **Edit `.atnotary.yaml`:** 36 + 37 + ```yaml 38 + privateKey: "0x..." # private key for writing 39 + network: base-sepolia # default network 40 + ``` 41 + 42 + 3. **Get testnet ETH:** 53 43 - Sepolia: https://sepoliafaucet.com/ 54 44 - Base Sepolia: https://bridge.base.org/ 55 45 ··· 63 53 atnotary init --network sepolia 64 54 ``` 65 55 66 - Then add the `SCHEMA_UID` to your `.env` file. 56 + Then add the `schemaUID` to your `.atnotary.yaml` file. 67 57 68 58 ## Usage 69 59 ··· 84 74 import { ATProtocolNotary } from 'atnotary'; 85 75 86 76 const notary = new ATProtocolNotary({ 87 - privateKey: process.env.PRIVATE_KEY!, 88 - schemaUID: process.env.SCHEMA_UID!, 77 + privateKey: "0x...", // optional, just for writing 89 78 }, 'sepolia'); 90 79 91 80 const result = await notary.notarizeRecord('at://...'); ··· 109 98 110 99 ## License 111 100 112 - MIT 113 - 101 + MIT
+9 -9
package.json
··· 1 1 { 2 2 "name": "atnotary", 3 - "version": "0.1.0", 3 + "version": "0.1.3", 4 4 "type": "module", 5 5 "description": "Notarize AT Protocol records on Ethereum using EAS", 6 6 "main": "./dist/lib/index.js", ··· 27 27 "test:run": "vitest run" 28 28 }, 29 29 "dependencies": { 30 - "@atproto/api": "^0.12.29", 30 + "@atproto/api": "^0.17.2", 31 31 "@ethereum-attestation-service/eas-sdk": "^2.9.0", 32 - "@ipld/dag-cbor": "^9.2.5", 33 - "ethers": "^6.15.0" 32 + "chalk": "^5.6.2", 33 + "commander": "^14.0.1", 34 + "ethers": "^6.15.0", 35 + "js-yaml": "^4.1.0", 36 + "ora": "^9.0.0" 34 37 }, 35 38 "devDependencies": { 36 - "@types/node": "^20.19.21", 39 + "@types/js-yaml": "^4.0.9", 40 + "@types/node": "^24.7.2", 37 41 "@vitest/coverage-v8": "^3.2.4", 38 - "chalk": "^5.6.2", 39 - "commander": "^11.1.0", 40 - "dotenv": "^16.6.1", 41 - "ora": "^8.2.0", 42 42 "tsx": "^4.20.6", 43 43 "typescript": "^5.9.3", 44 44 "vitest": "^3.2.4"
+101 -142
pnpm-lock.yaml
··· 9 9 .: 10 10 dependencies: 11 11 '@atproto/api': 12 - specifier: ^0.12.29 13 - version: 0.12.29 12 + specifier: ^0.17.2 13 + version: 0.17.2 14 14 '@ethereum-attestation-service/eas-sdk': 15 15 specifier: ^2.9.0 16 16 version: 2.9.0(typescript@5.9.3)(zod@3.25.76) 17 - '@ipld/dag-cbor': 18 - specifier: ^9.2.5 19 - version: 9.2.5 17 + chalk: 18 + specifier: ^5.6.2 19 + version: 5.6.2 20 + commander: 21 + specifier: ^14.0.1 22 + version: 14.0.1 20 23 ethers: 21 24 specifier: ^6.15.0 22 25 version: 6.15.0 26 + js-yaml: 27 + specifier: ^4.1.0 28 + version: 4.1.0 29 + ora: 30 + specifier: ^9.0.0 31 + version: 9.0.0 23 32 devDependencies: 33 + '@types/js-yaml': 34 + specifier: ^4.0.9 35 + version: 4.0.9 24 36 '@types/node': 25 - specifier: ^20.19.21 26 - version: 20.19.21 37 + specifier: ^24.7.2 38 + version: 24.7.2 27 39 '@vitest/coverage-v8': 28 40 specifier: ^3.2.4 29 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.21)(tsx@4.20.6)) 30 - chalk: 31 - specifier: ^5.6.2 32 - version: 5.6.2 33 - commander: 34 - specifier: ^11.1.0 35 - version: 11.1.0 36 - dotenv: 37 - specifier: ^16.6.1 38 - version: 16.6.1 39 - ora: 40 - specifier: ^8.2.0 41 - version: 8.2.0 41 + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(tsx@4.20.6)) 42 42 tsx: 43 43 specifier: ^4.20.6 44 44 version: 4.20.6 ··· 47 47 version: 5.9.3 48 48 vitest: 49 49 specifier: ^3.2.4 50 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.21)(tsx@4.20.6) 50 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(tsx@4.20.6) 51 51 52 52 packages: 53 53 ··· 61 61 resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 62 62 engines: {node: '>=6.0.0'} 63 63 64 - '@atproto/api@0.12.29': 65 - resolution: {integrity: sha512-PyzPLjGWR0qNOMrmj3Nt3N5NuuANSgOk/33Bu3j+rFjjPrHvk9CI6iQPU6zuDaDCoyOTRJRafw8X/aMQw+ilgw==} 66 - 67 - '@atproto/common-web@0.3.2': 68 - resolution: {integrity: sha512-Vx0JtL1/CssJbFAb0UOdvTrkbUautsDfHNOXNTcX2vyPIxH9xOameSqLLunM1hZnOQbJwyjmQCt6TV+bhnanDg==} 64 + '@atproto/api@0.17.2': 65 + resolution: {integrity: sha512-luRY9YPaRQFpm3v7a1bTOaekQ/KPCG3gb0jVyaOtfMXDSfIZJh9lr9MtmGPdEp7AvfE8urkngZ+V/p8Ial3z2g==} 69 66 70 67 '@atproto/common-web@0.4.3': 71 68 resolution: {integrity: sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==} 72 69 73 - '@atproto/lexicon@0.4.14': 74 - resolution: {integrity: sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==} 75 - 76 - '@atproto/syntax@0.3.4': 77 - resolution: {integrity: sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==} 70 + '@atproto/lexicon@0.5.1': 71 + resolution: {integrity: sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==} 78 72 79 73 '@atproto/syntax@0.4.1': 80 74 resolution: {integrity: sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==} 81 75 82 - '@atproto/xrpc@0.5.0': 83 - resolution: {integrity: sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==} 76 + '@atproto/xrpc@0.7.5': 77 + resolution: {integrity: sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==} 84 78 85 79 '@babel/helper-string-parser@7.27.1': 86 80 resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} ··· 339 333 resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 340 334 engines: {node: '>=14'} 341 335 342 - '@ipld/dag-cbor@9.2.5': 343 - resolution: {integrity: sha512-84wSr4jv30biui7endhobYhXBQzQE4c/wdoWlFrKcfiwH+ofaPg8fwsM8okX9cOzkkrsAsNdDyH3ou+kiLquwQ==} 344 - engines: {node: '>=16.0.0', npm: '>=7.0.0'} 345 - 346 336 '@isaacs/cliui@8.0.2': 347 337 resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 348 338 engines: {node: '>=12'} ··· 693 683 '@types/estree@1.0.8': 694 684 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 695 685 686 + '@types/js-yaml@4.0.9': 687 + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} 688 + 696 689 '@types/lru-cache@5.1.1': 697 690 resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} 698 691 699 692 '@types/ms@2.1.0': 700 693 resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} 701 694 702 - '@types/node@20.19.21': 703 - resolution: {integrity: sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==} 704 - 705 695 '@types/node@22.7.5': 706 696 resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} 697 + 698 + '@types/node@24.7.2': 699 + resolution: {integrity: sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==} 707 700 708 701 '@types/pbkdf2@3.1.2': 709 702 resolution: {integrity: sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==} ··· 905 898 resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} 906 899 engines: {node: '>=10'} 907 900 908 - cborg@4.2.15: 909 - resolution: {integrity: sha512-T+YVPemWyXcBVQdp0k61lQp2hJniRNmul0lAwTj2DTS/6dI4eCq/MRMucGqqvFqMBfmnD8tJ9aFtPu5dEGAbgw==} 910 - hasBin: true 911 - 912 901 chai@5.3.3: 913 902 resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} 914 903 engines: {node: '>=18'} ··· 952 941 resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} 953 942 engines: {node: '>=18'} 954 943 955 - cli-spinners@2.9.2: 956 - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} 957 - engines: {node: '>=6'} 944 + cli-spinners@3.3.0: 945 + resolution: {integrity: sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==} 946 + engines: {node: '>=18.20'} 958 947 959 948 cliui@7.0.4: 960 949 resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} ··· 975 964 command-exists@1.2.9: 976 965 resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} 977 966 978 - commander@11.1.0: 979 - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} 980 - engines: {node: '>=16'} 967 + commander@14.0.1: 968 + resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} 969 + engines: {node: '>=20'} 981 970 982 971 commander@3.0.2: 983 972 resolution: {integrity: sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==} ··· 1036 1025 resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} 1037 1026 engines: {node: '>=0.3.1'} 1038 1027 1039 - dotenv@16.6.1: 1040 - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} 1041 - engines: {node: '>=12'} 1042 - 1043 1028 dunder-proto@1.0.1: 1044 1029 resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 1045 1030 engines: {node: '>= 0.4'} ··· 1049 1034 1050 1035 elliptic@6.6.1: 1051 1036 resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} 1052 - 1053 - emoji-regex@10.5.0: 1054 - resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} 1055 1037 1056 1038 emoji-regex@8.0.0: 1057 1039 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} ··· 1374 1356 resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} 1375 1357 engines: {node: '>=10'} 1376 1358 1377 - is-unicode-supported@1.3.0: 1378 - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} 1379 - engines: {node: '>=12'} 1380 - 1381 1359 is-unicode-supported@2.1.0: 1382 1360 resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} 1383 1361 engines: {node: '>=18'} ··· 1459 1437 resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} 1460 1438 engines: {node: '>=10'} 1461 1439 1462 - log-symbols@6.0.0: 1463 - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} 1440 + log-symbols@7.0.1: 1441 + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} 1464 1442 engines: {node: '>=18'} 1465 1443 1466 1444 loupe@3.2.1: ··· 1531 1509 1532 1510 ms@2.1.3: 1533 1511 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1534 - 1535 - multiformats@13.4.1: 1536 - resolution: {integrity: sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==} 1537 1512 1538 1513 multiformats@9.9.0: 1539 1514 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} ··· 1567 1542 resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} 1568 1543 engines: {node: '>=18'} 1569 1544 1570 - ora@8.2.0: 1571 - resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} 1572 - engines: {node: '>=18'} 1545 + ora@9.0.0: 1546 + resolution: {integrity: sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==} 1547 + engines: {node: '>=20'} 1573 1548 1574 1549 os-tmpdir@1.0.2: 1575 1550 resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} ··· 1831 1806 resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 1832 1807 engines: {node: '>=12'} 1833 1808 1834 - string-width@7.2.0: 1835 - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} 1836 - engines: {node: '>=18'} 1809 + string-width@8.1.0: 1810 + resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} 1811 + engines: {node: '>=20'} 1837 1812 1838 1813 string_decoder@1.1.1: 1839 1814 resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} ··· 1965 1940 undici-types@6.19.8: 1966 1941 resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 1967 1942 1968 - undici-types@6.21.0: 1969 - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 1943 + undici-types@7.14.0: 1944 + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} 1970 1945 1971 1946 undici@5.29.0: 1972 1947 resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} ··· 1991 1966 resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} 1992 1967 hasBin: true 1993 1968 1994 - viem@2.38.0: 1995 - resolution: {integrity: sha512-YU5TG8dgBNeYPrCMww0u9/JVeq2ZCk9fzk6QybrPkBooFysamHXL1zC3ua10aLPt9iWoA/gSVf1D9w7nc5B1aA==} 1969 + viem@2.38.1: 1970 + resolution: {integrity: sha512-+5c5b8AmGBYJGMU0A3spIFgsBXseV1E+LlQnSDG80IBoXYDqeQ2XZ8wrwCl9FqLVeP+8NgXtouJaPpmv9VGwHQ==} 1996 1971 peerDependencies: 1997 1972 typescript: '>=5.0.4' 1998 1973 peerDependenciesMeta: ··· 2160 2135 resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 2161 2136 engines: {node: '>=10'} 2162 2137 2138 + yoctocolors@2.1.2: 2139 + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} 2140 + engines: {node: '>=18'} 2141 + 2163 2142 zod@3.25.76: 2164 2143 resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 2165 2144 ··· 2174 2153 '@jridgewell/gen-mapping': 0.3.13 2175 2154 '@jridgewell/trace-mapping': 0.3.31 2176 2155 2177 - '@atproto/api@0.12.29': 2156 + '@atproto/api@0.17.2': 2178 2157 dependencies: 2179 - '@atproto/common-web': 0.3.2 2180 - '@atproto/lexicon': 0.4.14 2181 - '@atproto/syntax': 0.3.4 2182 - '@atproto/xrpc': 0.5.0 2158 + '@atproto/common-web': 0.4.3 2159 + '@atproto/lexicon': 0.5.1 2160 + '@atproto/syntax': 0.4.1 2161 + '@atproto/xrpc': 0.7.5 2183 2162 await-lock: 2.2.2 2184 2163 multiformats: 9.9.0 2185 2164 tlds: 1.260.0 2186 - 2187 - '@atproto/common-web@0.3.2': 2188 - dependencies: 2189 - graphemer: 1.4.0 2190 - multiformats: 9.9.0 2191 - uint8arrays: 3.0.0 2192 2165 zod: 3.25.76 2193 2166 2194 2167 '@atproto/common-web@0.4.3': ··· 2198 2171 uint8arrays: 3.0.0 2199 2172 zod: 3.25.76 2200 2173 2201 - '@atproto/lexicon@0.4.14': 2174 + '@atproto/lexicon@0.5.1': 2202 2175 dependencies: 2203 2176 '@atproto/common-web': 0.4.3 2204 2177 '@atproto/syntax': 0.4.1 ··· 2206 2179 multiformats: 9.9.0 2207 2180 zod: 3.25.76 2208 2181 2209 - '@atproto/syntax@0.3.4': {} 2210 - 2211 2182 '@atproto/syntax@0.4.1': {} 2212 2183 2213 - '@atproto/xrpc@0.5.0': 2184 + '@atproto/xrpc@0.7.5': 2214 2185 dependencies: 2215 - '@atproto/lexicon': 0.4.14 2186 + '@atproto/lexicon': 0.5.1 2216 2187 zod: 3.25.76 2217 2188 2218 2189 '@babel/helper-string-parser@7.27.1': {} ··· 2329 2300 multiformats: 9.9.0 2330 2301 pako: 2.1.0 2331 2302 semver: 7.7.3 2332 - viem: 2.38.0(typescript@5.9.3)(zod@3.25.76) 2303 + viem: 2.38.1(typescript@5.9.3)(zod@3.25.76) 2333 2304 transitivePeerDependencies: 2334 2305 - bufferutil 2335 2306 - c-kzg ··· 2483 2454 '@ethersproject/strings': 5.8.0 2484 2455 2485 2456 '@fastify/busboy@2.1.1': {} 2486 - 2487 - '@ipld/dag-cbor@9.2.5': 2488 - dependencies: 2489 - cborg: 4.2.15 2490 - multiformats: 13.4.1 2491 2457 2492 2458 '@isaacs/cliui@8.0.2': 2493 2459 dependencies: ··· 2812 2778 2813 2779 '@types/bn.js@4.11.6': 2814 2780 dependencies: 2815 - '@types/node': 20.19.21 2781 + '@types/node': 24.7.2 2816 2782 2817 2783 '@types/bn.js@5.2.0': 2818 2784 dependencies: 2819 - '@types/node': 20.19.21 2785 + '@types/node': 24.7.2 2820 2786 2821 2787 '@types/chai@5.2.2': 2822 2788 dependencies: ··· 2829 2795 '@types/deep-eql@4.0.2': {} 2830 2796 2831 2797 '@types/estree@1.0.8': {} 2798 + 2799 + '@types/js-yaml@4.0.9': {} 2832 2800 2833 2801 '@types/lru-cache@5.1.1': {} 2834 2802 2835 2803 '@types/ms@2.1.0': {} 2836 2804 2837 - '@types/node@20.19.21': 2838 - dependencies: 2839 - undici-types: 6.21.0 2840 - 2841 2805 '@types/node@22.7.5': 2842 2806 dependencies: 2843 2807 undici-types: 6.19.8 2808 + 2809 + '@types/node@24.7.2': 2810 + dependencies: 2811 + undici-types: 7.14.0 2844 2812 2845 2813 '@types/pbkdf2@3.1.2': 2846 2814 dependencies: 2847 - '@types/node': 20.19.21 2815 + '@types/node': 24.7.2 2848 2816 2849 2817 '@types/secp256k1@4.0.7': 2850 2818 dependencies: 2851 - '@types/node': 20.19.21 2819 + '@types/node': 24.7.2 2852 2820 2853 - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.21)(tsx@4.20.6))': 2821 + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(tsx@4.20.6))': 2854 2822 dependencies: 2855 2823 '@ampproject/remapping': 2.3.0 2856 2824 '@bcoe/v8-coverage': 1.0.2 ··· 2865 2833 std-env: 3.9.0 2866 2834 test-exclude: 7.0.1 2867 2835 tinyrainbow: 2.0.0 2868 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.21)(tsx@4.20.6) 2836 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(tsx@4.20.6) 2869 2837 transitivePeerDependencies: 2870 2838 - supports-color 2871 2839 ··· 2877 2845 chai: 5.3.3 2878 2846 tinyrainbow: 2.0.0 2879 2847 2880 - '@vitest/mocker@3.2.4(vite@7.1.9(@types/node@20.19.21)(tsx@4.20.6))': 2848 + '@vitest/mocker@3.2.4(vite@7.1.9(@types/node@24.7.2)(tsx@4.20.6))': 2881 2849 dependencies: 2882 2850 '@vitest/spy': 3.2.4 2883 2851 estree-walker: 3.0.3 2884 2852 magic-string: 0.30.19 2885 2853 optionalDependencies: 2886 - vite: 7.1.9(@types/node@20.19.21)(tsx@4.20.6) 2854 + vite: 7.1.9(@types/node@24.7.2)(tsx@4.20.6) 2887 2855 2888 2856 '@vitest/pretty-format@3.2.4': 2889 2857 dependencies: ··· 3064 3032 3065 3033 camelcase@6.3.0: {} 3066 3034 3067 - cborg@4.2.15: {} 3068 - 3069 3035 chai@5.3.3: 3070 3036 dependencies: 3071 3037 assertion-error: 2.0.1 ··· 3117 3083 dependencies: 3118 3084 restore-cursor: 5.1.0 3119 3085 3120 - cli-spinners@2.9.2: {} 3086 + cli-spinners@3.3.0: {} 3121 3087 3122 3088 cliui@7.0.4: 3123 3089 dependencies: ··· 3139 3105 3140 3106 command-exists@1.2.9: {} 3141 3107 3142 - commander@11.1.0: {} 3108 + commander@14.0.1: {} 3143 3109 3144 3110 commander@3.0.2: {} 3145 3111 ··· 3194 3160 3195 3161 diff@5.2.0: {} 3196 3162 3197 - dotenv@16.6.1: {} 3198 - 3199 3163 dunder-proto@1.0.1: 3200 3164 dependencies: 3201 3165 call-bind-apply-helpers: 1.0.2 ··· 3213 3177 inherits: 2.0.4 3214 3178 minimalistic-assert: 1.0.1 3215 3179 minimalistic-crypto-utils: 1.0.1 3216 - 3217 - emoji-regex@10.5.0: {} 3218 3180 3219 3181 emoji-regex@8.0.0: {} 3220 3182 ··· 3630 3592 3631 3593 is-unicode-supported@0.1.0: {} 3632 3594 3633 - is-unicode-supported@1.3.0: {} 3634 - 3635 3595 is-unicode-supported@2.1.0: {} 3636 3596 3637 3597 isarray@1.0.0: {} ··· 3717 3677 chalk: 4.1.2 3718 3678 is-unicode-supported: 0.1.0 3719 3679 3720 - log-symbols@6.0.0: 3680 + log-symbols@7.0.1: 3721 3681 dependencies: 3722 - chalk: 5.6.2 3723 - is-unicode-supported: 1.3.0 3682 + is-unicode-supported: 2.1.0 3683 + yoctocolors: 2.1.2 3724 3684 3725 3685 loupe@3.2.1: {} 3726 3686 ··· 3803 3763 3804 3764 ms@2.1.3: {} 3805 3765 3806 - multiformats@13.4.1: {} 3807 - 3808 3766 multiformats@9.9.0: {} 3809 3767 3810 3768 nanoid@3.3.11: {} ··· 3827 3785 dependencies: 3828 3786 mimic-function: 5.0.1 3829 3787 3830 - ora@8.2.0: 3788 + ora@9.0.0: 3831 3789 dependencies: 3832 3790 chalk: 5.6.2 3833 3791 cli-cursor: 5.0.0 3834 - cli-spinners: 2.9.2 3792 + cli-spinners: 3.3.0 3835 3793 is-interactive: 2.0.0 3836 3794 is-unicode-supported: 2.1.0 3837 - log-symbols: 6.0.0 3795 + log-symbols: 7.0.1 3838 3796 stdin-discarder: 0.2.2 3839 - string-width: 7.2.0 3797 + string-width: 8.1.0 3840 3798 strip-ansi: 7.1.2 3841 3799 3842 3800 os-tmpdir@1.0.2: {} ··· 4115 4073 emoji-regex: 9.2.2 4116 4074 strip-ansi: 7.1.2 4117 4075 4118 - string-width@7.2.0: 4076 + string-width@8.1.0: 4119 4077 dependencies: 4120 - emoji-regex: 10.5.0 4121 4078 get-east-asian-width: 1.4.0 4122 4079 strip-ansi: 7.1.2 4123 4080 ··· 4235 4192 4236 4193 undici-types@6.19.8: {} 4237 4194 4238 - undici-types@6.21.0: {} 4195 + undici-types@7.14.0: {} 4239 4196 4240 4197 undici@5.29.0: 4241 4198 dependencies: ··· 4251 4208 4252 4209 uuid@9.0.1: {} 4253 4210 4254 - viem@2.38.0(typescript@5.9.3)(zod@3.25.76): 4211 + viem@2.38.1(typescript@5.9.3)(zod@3.25.76): 4255 4212 dependencies: 4256 4213 '@noble/curves': 1.9.1 4257 4214 '@noble/hashes': 1.8.0 ··· 4268 4225 - utf-8-validate 4269 4226 - zod 4270 4227 4271 - vite-node@3.2.4(@types/node@20.19.21)(tsx@4.20.6): 4228 + vite-node@3.2.4(@types/node@24.7.2)(tsx@4.20.6): 4272 4229 dependencies: 4273 4230 cac: 6.7.14 4274 4231 debug: 4.4.3(supports-color@8.1.1) 4275 4232 es-module-lexer: 1.7.0 4276 4233 pathe: 2.0.3 4277 - vite: 7.1.9(@types/node@20.19.21)(tsx@4.20.6) 4234 + vite: 7.1.9(@types/node@24.7.2)(tsx@4.20.6) 4278 4235 transitivePeerDependencies: 4279 4236 - '@types/node' 4280 4237 - jiti ··· 4289 4246 - tsx 4290 4247 - yaml 4291 4248 4292 - vite@7.1.9(@types/node@20.19.21)(tsx@4.20.6): 4249 + vite@7.1.9(@types/node@24.7.2)(tsx@4.20.6): 4293 4250 dependencies: 4294 4251 esbuild: 0.25.10 4295 4252 fdir: 6.5.0(picomatch@4.0.3) ··· 4298 4255 rollup: 4.52.4 4299 4256 tinyglobby: 0.2.15 4300 4257 optionalDependencies: 4301 - '@types/node': 20.19.21 4258 + '@types/node': 24.7.2 4302 4259 fsevents: 2.3.3 4303 4260 tsx: 4.20.6 4304 4261 4305 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.21)(tsx@4.20.6): 4262 + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.7.2)(tsx@4.20.6): 4306 4263 dependencies: 4307 4264 '@types/chai': 5.2.2 4308 4265 '@vitest/expect': 3.2.4 4309 - '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@20.19.21)(tsx@4.20.6)) 4266 + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@24.7.2)(tsx@4.20.6)) 4310 4267 '@vitest/pretty-format': 3.2.4 4311 4268 '@vitest/runner': 3.2.4 4312 4269 '@vitest/snapshot': 3.2.4 ··· 4324 4281 tinyglobby: 0.2.15 4325 4282 tinypool: 1.1.1 4326 4283 tinyrainbow: 2.0.0 4327 - vite: 7.1.9(@types/node@20.19.21)(tsx@4.20.6) 4328 - vite-node: 3.2.4(@types/node@20.19.21)(tsx@4.20.6) 4284 + vite: 7.1.9(@types/node@24.7.2)(tsx@4.20.6) 4285 + vite-node: 3.2.4(@types/node@24.7.2)(tsx@4.20.6) 4329 4286 why-is-node-running: 2.3.0 4330 4287 optionalDependencies: 4331 4288 '@types/debug': 4.1.12 4332 - '@types/node': 20.19.21 4289 + '@types/node': 24.7.2 4333 4290 transitivePeerDependencies: 4334 4291 - jiti 4335 4292 - less ··· 4411 4368 yargs-parser: 20.2.9 4412 4369 4413 4370 yocto-queue@0.1.0: {} 4371 + 4372 + yoctocolors@2.1.2: {} 4414 4373 4415 4374 zod@3.25.76: {}
+64 -33
src/cli.ts
··· 3 3 import { Command } from 'commander'; 4 4 import chalk from 'chalk'; 5 5 import ora from 'ora'; 6 - import * as dotenv from 'dotenv'; 6 + import * as path from 'path'; 7 + import * as os from 'os'; 8 + import * as fs from 'node:fs'; 7 9 import { ATProtocolNotary } from './lib/index.js'; 8 - 9 - dotenv.config(); 10 + import { loadConfig, getSchemaUID, createConfigTemplate } from './lib/config.js'; 10 11 11 12 const program = new Command(); 12 13 13 14 program 14 15 .name('atnotary') 15 16 .description('Notarize AT Protocol records on Ethereum using EAS') 16 - .version('0.1.0'); 17 + .version('0.1.0') 18 + .option('-c, --config <path>', 'Path to config file'); 19 + 20 + // Config command 21 + program 22 + .command('config') 23 + .description('Create config template') 24 + .option('-g, --global', 'Create in home directory') 25 + .option('-f, --force', 'Overwrite existing config file') 26 + .action((options) => { 27 + const configPath = options.global 28 + ? path.join(os.homedir(), '.atnotary.yaml') 29 + : path.join(process.cwd(), '.atnotary.yaml'); 30 + 31 + // Check if file exists 32 + if (fs.existsSync(configPath) && !options.force) { 33 + console.log(chalk.yellow(`\nโš ๏ธ Config file already exists: ${configPath}`)); 34 + console.log(chalk.gray('Use --force to overwrite\n')); 35 + process.exit(1); 36 + } 37 + 38 + createConfigTemplate(configPath); 39 + console.log(chalk.green(`\nโœ… Config template created: ${configPath}`)); 40 + console.log(chalk.yellow('\nEdit the file and add your private key and schema UIDs.\n')); 41 + }); 17 42 18 43 // Initialize command 19 44 program ··· 22 47 .option('-n, --network <network>', 'Network to use', 'sepolia') 23 48 .action(async (options) => { 24 49 try { 50 + const config = loadConfig(program.opts().config); 51 + 25 52 console.log(chalk.blue('๐Ÿ”ง Initializing EAS Schema')); 26 53 console.log(chalk.gray(`Network: ${options.network}\n`)); 27 54 28 55 const spinner = ora(`Connecting to ${options.network}...`).start(); 29 56 30 57 const notary = new ATProtocolNotary({ 31 - privateKey: process.env.PRIVATE_KEY!, 58 + privateKey: config.privateKey, 32 59 }, options.network); 33 60 34 61 const address = await notary.getAddress(); 35 62 spinner.succeed(`Connected to ${options.network}: ${address}`); 63 + console.log(chalk.blue('Account:'), chalk.cyan(address)); 36 64 37 65 spinner.start(`Creating schema on EAS (${options.network})...`); 38 66 const schemaUID = await notary.initializeSchema(); ··· 40 68 spinner.succeed('Schema created!'); 41 69 42 70 console.log(chalk.green('\nโœ… Schema UID:'), chalk.cyan(schemaUID)); 43 - console.log(chalk.yellow('\nโš ๏ธ Add this to your .env file:')); 44 - console.log(chalk.gray(`SCHEMA_UID="${schemaUID}"\n`)); 71 + console.log(chalk.yellow('\nโš ๏ธ Add this to your .atnotary.json:')); 72 + console.log(chalk.gray(`\n"networks": {\n "${options.network}": {\n "schemaUID": "${schemaUID}"\n }\n}\n`)); 45 73 46 74 } catch (error: any) { 47 75 console.error(chalk.red('Error:'), error.message); ··· 53 81 program 54 82 .command('notarize <recordURI>') 55 83 .description('Notarize an AT Protocol record') 56 - .option('-n, --network <network>', 'Network to use', 'sepolia') 84 + .option('-n, --network <network>', 'Network to use') 57 85 .action(async (recordURI: string, options) => { 58 86 try { 87 + const config = loadConfig(program.opts().config); 88 + const network = options.network || config.network || 'sepolia'; 89 + 59 90 console.log(chalk.blue('๐Ÿ” AT Protocol Notary')); 60 - console.log(chalk.gray(`Network: ${options.network}\n`)); 91 + console.log(chalk.gray(`Network: ${network}\n`)); 61 92 62 93 const spinner = ora('Initializing...').start(); 63 94 64 95 const notary = new ATProtocolNotary({ 65 - privateKey: process.env.PRIVATE_KEY!, 66 - schemaUID: process.env.SCHEMA_UID, 67 - }, options.network); 96 + privateKey: config.privateKey, 97 + schemaUID: getSchemaUID(config, network), 98 + }, network); 68 99 69 - spinner.succeed('Initialized'); 100 + // Show account info 101 + const address = await notary.getAddress(); 102 + spinner.succeed(`Initialized with account: ${chalk.cyan(address)}`); 70 103 71 104 spinner.start('Resolving DID to PDS...'); 72 105 const { record, pds } = await notary.fetchRecord(recordURI); ··· 75 108 console.log(chalk.gray('\nRecord preview:')); 76 109 console.log(chalk.gray(JSON.stringify(record.value, null, 2).substring(0, 200) + '...\n')); 77 110 78 - spinner.start(`Creating attestation on ${options.network}...`); 111 + spinner.start(`Creating attestation on ${network}...`); 79 112 const result = await notary.notarizeRecord(recordURI); 80 113 spinner.succeed('Attestation created!'); 81 114 82 115 console.log(chalk.green('\nโœ… Notarization Complete!\n')); 83 - console.log(chalk.blue('Network:'), chalk.gray(options.network)); 116 + console.log(chalk.blue('Network:'), chalk.gray(network)); 117 + console.log(chalk.blue('Attester:'), chalk.cyan(address)); 84 118 console.log(chalk.blue('Attestation UID:'), chalk.cyan(result.attestationUID)); 85 119 console.log(chalk.blue('Record URI:'), chalk.gray(result.recordURI)); 86 120 console.log(chalk.blue('CID:'), chalk.gray(result.cid)); ··· 92 126 console.log(chalk.cyan(result.explorerURL + '\n')); 93 127 94 128 console.log(chalk.yellow('๐Ÿ” Verify with:')); 95 - console.log(chalk.gray(`atnotary verify ${result.attestationUID} --network ${options.network}\n`)); 129 + console.log(chalk.gray(`atnotary verify ${result.attestationUID} --network ${network}\n`)); 96 130 97 131 } catch (error: any) { 98 132 console.error(chalk.red('Error:'), error.message); 99 133 process.exit(1); 100 134 } 101 135 }); 136 + 102 137 103 138 // Verify command 104 139 program 105 140 .command('verify <attestationUID>') 106 141 .description('Verify an attestation') 107 - .option('-n, --network <network>', 'Network to use', 'sepolia') 108 - .option('-c, --compare', 'Compare with current record state') 142 + .option('-n, --network <network>', 'Network to use') 143 + .option('--compare', 'Compare with current record state') 109 144 .action(async (attestationUID: string, options) => { 110 145 try { 146 + const config = loadConfig(program.opts().config); 147 + const network = options.network || config.network || 'sepolia'; 148 + 111 149 console.log(chalk.blue('๐Ÿ” Verifying Attestation')); 112 - console.log(chalk.gray(`Network: ${options.network}\n`)); 150 + console.log(chalk.gray(`Network: ${network}\n`)); 113 151 114 - const spinner = ora(`Fetching attestation from EAS (${options.network})...`).start(); 152 + const spinner = ora(`Fetching attestation from EAS (${network})...`).start(); 115 153 116 154 const notary = new ATProtocolNotary({ 117 - schemaUID: process.env.SCHEMA_UID, 118 - }, options.network); 155 + schemaUID: getSchemaUID(config, network), 156 + }, network); 119 157 120 158 const attestation = await notary.verifyAttestation(attestationUID); 121 159 spinner.succeed('Attestation found'); 122 160 123 161 console.log(chalk.green('\nโœ… Attestation Valid\n')); 124 - console.log(chalk.blue('Network:'), chalk.gray(options.network)); 162 + console.log(chalk.blue('Network:'), chalk.gray(network)); 125 163 console.log(chalk.blue('UID:'), chalk.cyan(attestation.uid)); 126 164 console.log(chalk.blue('Record URI:'), chalk.gray(attestation.recordURI)); 127 165 console.log(chalk.blue('CID:'), chalk.gray(attestation.cid)); ··· 130 168 console.log(chalk.blue('Timestamp:'), chalk.gray(new Date(attestation.timestamp * 1000).toISOString())); 131 169 console.log(chalk.blue('Attester:'), chalk.gray(attestation.attester)); 132 170 133 - // Compare with current if requested 134 171 if (options.compare) { 135 172 console.log(chalk.yellow('\n๐Ÿ”„ Comparing with current record...')); 136 173 ··· 140 177 if (!comparison.exists) { 141 178 console.log(chalk.red('โš  Record has been deleted')); 142 179 } else { 143 - // Check CID 144 180 if (comparison.cidMatches) { 145 - console.log(chalk.green('โœ“ CID matches (content identical via AT Protocol)')); 181 + console.log(chalk.green('โœ“ CID matches (content identical)')); 146 182 } else { 147 183 console.log(chalk.red('โœ— CID changed (content modified)')); 148 - console.log(chalk.gray(` Attested CID: ${attestation.cid}`)); 149 - console.log(chalk.gray(` Current CID: ${comparison.currentCid}`)); 184 + console.log(chalk.gray(` Attested: ${attestation.cid}`)); 185 + console.log(chalk.gray(` Current: ${comparison.currentCid}`)); 150 186 } 151 187 152 - // Check content hash 153 188 if (comparison.hashMatches) { 154 189 console.log(chalk.green('โœ“ Content hash matches')); 155 190 } else { 156 191 console.log(chalk.red('โœ— Content hash changed')); 157 - console.log(chalk.gray(` Attested Hash: ${attestation.contentHash.substring(0, 20)}...`)); 158 - console.log(chalk.gray(` Current Hash: ${comparison.currentHash!.substring(0, 20)}...`)); 159 192 } 160 193 161 - // Summary 162 194 if (comparison.cidMatches && comparison.hashMatches) { 163 195 console.log(chalk.green('\nโœ… Record unchanged since attestation')); 164 196 } else { ··· 169 201 console.log(chalk.red(`โš  Could not fetch current record: ${err.message}`)); 170 202 } 171 203 } 172 - 173 204 174 205 console.log(chalk.blue('\n๐Ÿ“‹ View on explorer:')); 175 206 console.log(chalk.cyan(attestation.explorerURL + '\n'));
+101
src/lib/config.ts
··· 1 + import * as fs from 'fs'; 2 + import * as path from 'path'; 3 + import * as os from 'os'; 4 + import yaml from 'js-yaml'; 5 + 6 + export interface Config { 7 + privateKey?: string; 8 + network?: string; 9 + networks?: { 10 + [network: string]: { 11 + schemaUID?: string; 12 + rpcUrl?: string; 13 + easContractAddress?: string; 14 + schemaRegistryAddress?: string; 15 + }; 16 + }; 17 + } 18 + 19 + /** 20 + * Load config from multiple sources (priority order): 21 + * 1. Custom config path (--config flag) 22 + * 2. ./.atnotary.yaml (current directory) 23 + * 3. ~/.atnotary.yaml (home directory) 24 + * 4. .env file (fallback) 25 + */ 26 + export function loadConfig(customPath?: string): Config { 27 + let config: Config = {}; 28 + 29 + // Try custom path first 30 + if (customPath) { 31 + if (fs.existsSync(customPath)) { 32 + config = loadYamlConfig(customPath); 33 + return config; 34 + } else { 35 + throw new Error(`Config file not found: ${customPath}`); 36 + } 37 + } 38 + 39 + // Try current directory 40 + const localConfig = path.join(process.cwd(), '.atnotary.yaml'); 41 + if (fs.existsSync(localConfig)) { 42 + config = loadYamlConfig(localConfig); 43 + return config; 44 + } 45 + 46 + // Try home directory 47 + const homeConfig = path.join(os.homedir(), '.atnotary.yaml'); 48 + if (fs.existsSync(homeConfig)) { 49 + config = loadYamlConfig(homeConfig); 50 + return config; 51 + } 52 + 53 + return config; 54 + } 55 + 56 + function loadYamlConfig(filePath: string): Config { 57 + try { 58 + const fileContents = fs.readFileSync(filePath, 'utf8'); 59 + const config = yaml.load(fileContents) as Config; 60 + return config; 61 + } catch (error: any) { 62 + throw new Error(`Failed to load config from ${filePath}: ${error.message}`); 63 + } 64 + } 65 + 66 + /** 67 + * Get schema UID for specific network 68 + */ 69 + export function getSchemaUID(config: Config, network: string): string | undefined { 70 + return config.networks?.[network]?.schemaUID; 71 + } 72 + 73 + /** 74 + * Create default config template 75 + */ 76 + export function createConfigTemplate(outputPath: string): void { 77 + const template = `# atnotary configuration 78 + # Private key for signing transactions (required for notarization) 79 + privateKey: "0x..." 80 + 81 + # Default network (sepolia, base-sepolia, base) 82 + network: sepolia 83 + 84 + # Network-specific configuration 85 + networks: 86 + sepolia: 87 + schemaUID: "0x..." 88 + # Optional: custom RPC URL 89 + # rpcUrl: "https://..." 90 + 91 + base-sepolia: 92 + schemaUID: "0x..." 93 + # rpcUrl: "https://sepolia.base.org" 94 + 95 + base: 96 + schemaUID: "0x..." 97 + # rpcUrl: "https://mainnet.base.org" 98 + `; 99 + 100 + fs.writeFileSync(outputPath, template, 'utf8'); 101 + }
+8 -16
src/lib/notary.ts
··· 3 3 import EASPackage from '@ethereum-attestation-service/eas-sdk'; 4 4 const { EAS, SchemaEncoder, SchemaRegistry, NO_EXPIRATION } = EASPackage; 5 5 6 - import type { NotaryConfig, NotarizationResult, AttestationData } from './types'; 7 - import { parseRecordURI, hashContent, getExplorerURL, extractHashFromCID } from './utils'; 6 + import type { NotaryConfig, NotarizationResult, AttestationData } from './types.ts'; 7 + import { parseRecordURI, hashContent, getExplorerURL } from './utils.js'; 8 8 9 9 // Default schemas (deployed by atnotary maintainers) 10 10 const DEFAULT_SCHEMAS = { 11 11 'sepolia': '0x2a39517604107c79acbb962fe809795a87b7e47b8682fd9fbd3f62694fcca47c', 12 - 'base-sepolia': '0x...', // TODO: Deploy and add schema UID 12 + 'base-sepolia': '0x2a39517604107c79acbb962fe809795a87b7e47b8682fd9fbd3f62694fcca47c', 13 13 'base': '0x...', // TODO: Deploy and add schema UID 14 14 }; 15 15 ··· 37 37 export class ATProtocolNotary { 38 38 private config: Required<NotaryConfig>; 39 39 private provider: ethers.JsonRpcProvider; 40 - private signer: ethers.Wallet; 40 + private signer?: ethers.Wallet; 41 41 private network: string; 42 42 private chainConfig: typeof CHAIN_CONFIG[keyof typeof CHAIN_CONFIG]; 43 43 private eas: any; 44 - private schemaRegistry: any; 44 + private schemaRegistry?: any; 45 45 46 46 constructor(config: NotaryConfig = {}, network: string = 'sepolia') { 47 47 this.network = network; ··· 248 248 const pds = decodedData.find(d => d.name === 'pds')?.value.value as string; 249 249 const timestamp = Number(decodedData.find(d => d.name === 'timestamp')?.value.value); 250 250 251 - console.log({ extracted: extractHashFromCID(cid), real: contentHash }) 252 - 253 251 // Parse lexicon from recordURI (since it's not in schema) 254 252 const { collection: lexicon } = parseRecordURI(recordURI); 255 253 ··· 279 277 currentHash?: string; 280 278 }> { 281 279 try { 280 + // fetchRecord now returns { record, pds } 282 281 const { record } = await this.fetchRecord(attestationData.recordURI); 283 - const currentCid = record.cid; 284 - 285 - // Compute hash using DAG-CBOR (same as CID) 286 282 const currentHash = hashContent(record.value); 287 - 288 - // Extract hash from CID for comparison 289 - const cidHash = extractHashFromCID(currentCid); 283 + const currentCid = record.cid; 290 284 291 285 return { 292 286 exists: true, 293 287 cidMatches: currentCid === attestationData.cid, 294 - // Now contentHash should match the hash inside CID! 295 - hashMatches: currentHash === attestationData.contentHash && currentHash === cidHash, 288 + hashMatches: currentHash === attestationData.contentHash, 296 289 currentCid, 297 290 currentHash, 298 291 }; ··· 307 300 throw error; 308 301 } 309 302 } 310 - 311 303 312 304 /** 313 305 * Get signer address
+10 -30
src/lib/utils.ts
··· 1 1 import * as crypto from 'crypto'; 2 - import { encode } from '@ipld/dag-cbor'; 3 - import { CID } from 'multiformats/cid'; 4 - import * as sha256 from 'multiformats/hashes/sha256'; 5 2 6 3 export function parseRecordURI(uri: string): { did: string; collection: string; rkey: string } { 7 4 const match = uri.match(/^at:\/\/(did:[^\/]+)\/([^\/]+)\/(.+)$/); ··· 10 7 } 11 8 12 9 return { 13 - did: match[1], 14 - collection: match[2], 15 - rkey: match[3] 10 + did: match[1]!, 11 + collection: match[2]!, 12 + rkey: match[3]! 16 13 }; 17 14 } 18 15 ··· 32 29 return sorted; 33 30 } 34 31 32 + export function hashContent(content: any): string { 33 + // Sort keys for deterministic hashing 34 + const sortedContent = sortObject(content); 35 + const jsonString = JSON.stringify(sortedContent); 36 + return '0x' + crypto.createHash('sha256').update(jsonString).digest('hex'); 37 + } 38 + 35 39 export function getExplorerURL(attestationUID: string, network: string = 'sepolia'): string { 36 40 const explorers: Record<string, string> = { 37 41 'sepolia': 'https://sepolia.easscan.org', ··· 44 48 const baseURL = explorers[network] || explorers['sepolia']; 45 49 return `${baseURL}/attestation/view/${attestationUID}`; 46 50 } 47 - 48 - // Extract hash bytes from CID 49 - export function extractHashFromCID(cidString: string): string { 50 - const cid = CID.parse(cidString); 51 - return '0x' + Buffer.from(cid.multihash.digest).toString('hex'); 52 - } 53 - 54 - // Hash content using DAG-CBOR (same as AT Protocol) 55 - export function hashContent(content: any): string { 56 - // Encode as DAG-CBOR (same as AT Protocol) 57 - const cborBytes = encode(content); 58 - 59 - // Hash with SHA-256 60 - const hash = crypto.createHash('sha256').update(cborBytes).digest('hex'); 61 - 62 - return '0x' + hash; 63 - } 64 - 65 - // Verify content matches CID 66 - export function verifyCID(content: any, cidString: string): boolean { 67 - const computedHash = hashContent(content); 68 - const cidHash = extractHashFromCID(cidString); 69 - return computedHash === cidHash; 70 - }
+16 -43
tests/integration/notary.test.ts
··· 1 1 import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 2 import { ATProtocolNotary } from '../../src/lib/notary'; 3 - import { CID } from 'multiformats/cid'; 4 - import * as sha256 from 'multiformats/hashes/sha2'; 5 - import { encode } from '@ipld/dag-cbor'; 6 - 7 - // Generate real valid CIDs for test data 8 - const TEST_CONTENT_1 = { text: 'Test post', createdAt: '2024-01-01' }; 9 - const TEST_CONTENT_2 = { text: 'Modified post', createdAt: '2024-01-02' }; 10 - 11 - // Create real CIDs 12 - async function createCID(content: any): Promise<string> { 13 - const bytes = encode(content); 14 - const hash = await sha256.sha256.digest(bytes); 15 - const cid = CID.create(1, 0x71, hash); // CIDv1, dag-cbor 16 - return cid.toString(); 17 - } 18 - 19 - // Generate CIDs at module level (needs to be async) 20 - let TEST_CID_1: string; 21 - let TEST_CID_2: string; 22 - let TEST_HASH_1: string; 23 3 24 4 // Mock external dependencies 25 5 vi.mock('@atproto/api', () => ({ ··· 29 9 repo: { 30 10 getRecord: vi.fn().mockResolvedValue({ 31 11 data: { 32 - value: TEST_CONTENT_1, 33 - cid: TEST_CID_1, 12 + value: { text: 'Test post', createdAt: '2024-01-01' }, 13 + cid: 'bafyreiabc123', 34 14 }, 35 15 }), 36 16 }, ··· 79 59 encodeData: vi.fn().mockReturnValue('0xencodeddata'), 80 60 decodeData: vi.fn().mockReturnValue([ 81 61 { name: 'recordURI', value: { value: 'at://did:plc:test/app.bsky.feed.post/123' } }, 82 - { name: 'cid', value: { value: TEST_CID_1 } }, 83 - { name: 'contentHash', value: { value: TEST_HASH_1 } }, 62 + { name: 'cid', value: { value: 'bafyreiabc123' } }, 63 + { name: 'contentHash', value: { value: '0xa64b286ffd5cc55c57f4e9c74d1122aa081dc5f662648cb5cc5ced74e0e12cd5' } }, 84 64 { name: 'pds', value: { value: 'https://pds.example.com' } }, 85 65 { name: 'timestamp', value: { value: 1234567890 } }, 86 66 ]), ··· 93 73 global.fetch = vi.fn(); 94 74 95 75 describe('ATProtocolNotary', () => { 96 - beforeEach(async () => { 76 + beforeEach(() => { 97 77 vi.clearAllMocks(); 98 78 99 - // Generate real CIDs 100 - TEST_CID_1 = await createCID(TEST_CONTENT_1); 101 - TEST_CID_2 = await createCID(TEST_CONTENT_2); 102 - 103 - // Generate real hash 104 - const { hashContent } = await import('../../src/lib/utils'); 105 - TEST_HASH_1 = hashContent(TEST_CONTENT_1); 106 - 107 79 // Mock DID resolution 108 80 (global.fetch as any).mockResolvedValue({ 109 81 ok: true, ··· 178 150 const result = await notary.fetchRecord('at://did:plc:test/app.bsky.feed.post/123'); 179 151 180 152 expect(result.record.value.text).toBe('Test post'); 181 - expect(result.record.cid).toBe(TEST_CID_1); 153 + expect(result.record.cid).toBe('bafyreiabc123'); 182 154 expect(result.pds).toBe('https://pds.example.com'); 183 155 }); 184 156 }); ··· 194 166 195 167 expect(result.attestationUID).toBe('0xattestationuid123'); 196 168 expect(result.recordURI).toBe('at://did:plc:test/app.bsky.feed.post/123'); 197 - expect(result.cid).toBe(TEST_CID_1); 169 + expect(result.cid).toBe('bafyreiabc123'); 198 170 expect(result.pds).toBe('https://pds.example.com'); 199 171 expect(result.transactionHash).toBeTruthy(); 200 172 }); ··· 210 182 211 183 expect(result.uid).toBe('0xattestationuid123'); 212 184 expect(result.recordURI).toBe('at://did:plc:test/app.bsky.feed.post/123'); 213 - expect(result.cid).toBe(TEST_CID_1); 185 + expect(result.cid).toBe('bafyreiabc123'); 214 186 expect(result.pds).toBe('https://pds.example.com'); 215 187 expect(result.attester).toBe('0xattester'); 216 188 expect(result.revoked).toBe(false); ··· 229 201 expect(comparison.exists).toBe(true); 230 202 expect(comparison.cidMatches).toBe(true); 231 203 expect(comparison.hashMatches).toBe(true); 232 - expect(comparison.currentCid).toBe(TEST_CID_1); 233 - expect(comparison.currentHash).toBe(TEST_HASH_1); 204 + expect(comparison.currentCid).toBeTruthy(); 205 + expect(comparison.currentHash).toBeTruthy(); 234 206 }); 235 207 236 208 it('should detect changed content', async () => { 237 - // Mock different content 209 + // Mock different CID and hash 238 210 const { AtpAgent } = await import('@atproto/api'); 239 - (AtpAgent as any).mockImplementationOnce(() => ({ 211 + (AtpAgent as any).mockImplementation(() => ({ 240 212 com: { 241 213 atproto: { 242 214 repo: { 243 215 getRecord: vi.fn().mockResolvedValue({ 244 216 data: { 245 - value: TEST_CONTENT_2, 246 - cid: TEST_CID_2, 217 + value: { text: 'Modified post', createdAt: '2024-01-02' }, 218 + cid: 'bafyreidifferent123', 247 219 }, 248 220 }), 249 221 }, ··· 260 232 261 233 expect(comparison.exists).toBe(true); 262 234 expect(comparison.cidMatches).toBe(false); 263 - expect(comparison.currentCid).toBe(TEST_CID_2); 235 + expect(comparison.hashMatches).toBe(false); 264 236 }); 265 237 }); 238 + 266 239 });
+20 -19
tests/unit/utils.test.ts
··· 1 1 import { describe, it, expect } from 'vitest'; 2 - import { parseRecordURI, hashContent, getExplorerURL, extractHashFromCID, verifyCID } from '../../src/lib/utils'; 2 + import { parseRecordURI, hashContent, getExplorerURL } from '../../src/lib/utils'; 3 3 4 4 describe('parseRecordURI', () => { 5 5 it('should parse valid AT Protocol URI', () => { ··· 27 27 }); 28 28 }); 29 29 30 - describe('hashContent with DAG-CBOR', () => { 31 - it('should produce hash that matches CID', () => { 30 + describe('hashContent', () => { 31 + it('should generate consistent SHA-256 hash', () => { 32 32 const content = { text: 'Hello World', createdAt: '2024-01-01' }; 33 - const contentHash = hashContent(content); 33 + const hash1 = hashContent(content); 34 + const hash2 = hashContent(content); 34 35 35 - // This hash should match the hash inside a CID of the same content 36 - expect(contentHash).toMatch(/^0x[a-f0-9]{64}$/); 36 + expect(hash1).toBe(hash2); 37 + expect(hash1).toMatch(/^0x[a-f0-9]{64}$/); 37 38 }); 38 - }); 39 39 40 - describe('extractHashFromCID', () => { 41 - it('should extract hash from valid CID', () => { 42 - const cid = 'bafyreig3iwk4yuvewp54jkzplqw4vxae5o3smtcfn2jxk7x4ewhicbuw4m'; // Real CID 43 - const hash = extractHashFromCID(cid); 40 + it('should generate different hashes for different content', () => { 41 + const content1 = { text: 'Hello' }; 42 + const content2 = { text: 'World' }; 44 43 45 - expect(hash).toMatch(/^0x[a-f0-9]{64}$/); 44 + expect(hashContent(content1)).not.toBe(hashContent(content2)); 46 45 }); 47 - }); 48 46 49 - describe('verifyCID', () => { 50 - it('should verify content matches CID', () => { 51 - const content = { text: 'Test' }; 52 - const contentHash = hashContent(content); 47 + it('should be order-sensitive', () => { 48 + const content1 = { a: 1, b: 2 }; 49 + const content2 = { b: 2, a: 1 }; 50 + 51 + // JSON.stringify is order-sensitive 52 + const hash1 = hashContent(content1); 53 + const hash2 = hashContent(content2); 53 54 54 - // In real usage, CID and content should match 55 - // This would need a real CID from AT Protocol 55 + expect(hash1).toBeTruthy(); 56 + expect(hash2).toBeTruthy(); 56 57 }); 57 58 }); 58 59