+2
-1
.gitignore
+2
-1
.gitignore
+14
-26
README.md
+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
+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
+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
+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
+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
+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
+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
+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
+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