+2
-3
package.json
+2
-3
package.json
···
29
29
"dependencies": {
30
30
"@atproto/api": "^0.12.29",
31
31
"@ethereum-attestation-service/eas-sdk": "^2.9.0",
32
-
"@ipld/dag-cbor": "^9.2.5",
33
-
"ethers": "^6.15.0",
34
-
"js-yaml": "^4.1.0"
32
+
"js-yaml": "^4.1.0",
33
+
"ethers": "^6.15.0"
35
34
},
36
35
"devDependencies": {
37
36
"@types/js-yaml": "^4.0.9",
-23
pnpm-lock.yaml
-23
pnpm-lock.yaml
···
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
20
17
ethers:
21
18
specifier: ^6.15.0
22
19
version: 6.15.0
···
342
339
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
343
340
engines: {node: '>=14'}
344
341
345
-
'@ipld/dag-cbor@9.2.5':
346
-
resolution: {integrity: sha512-84wSr4jv30biui7endhobYhXBQzQE4c/wdoWlFrKcfiwH+ofaPg8fwsM8okX9cOzkkrsAsNdDyH3ou+kiLquwQ==}
347
-
engines: {node: '>=16.0.0', npm: '>=7.0.0'}
348
-
349
342
'@isaacs/cliui@8.0.2':
350
343
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
351
344
engines: {node: '>=12'}
···
910
903
camelcase@6.3.0:
911
904
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
912
905
engines: {node: '>=10'}
913
-
914
-
cborg@4.2.15:
915
-
resolution: {integrity: sha512-T+YVPemWyXcBVQdp0k61lQp2hJniRNmul0lAwTj2DTS/6dI4eCq/MRMucGqqvFqMBfmnD8tJ9aFtPu5dEGAbgw==}
916
-
hasBin: true
917
906
918
907
chai@5.3.3:
919
908
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
···
1534
1523
ms@2.1.3:
1535
1524
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
1536
1525
1537
-
multiformats@13.4.1:
1538
-
resolution: {integrity: sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==}
1539
-
1540
1526
multiformats@9.9.0:
1541
1527
resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==}
1542
1528
···
2486
2472
2487
2473
'@fastify/busboy@2.1.1': {}
2488
2474
2489
-
'@ipld/dag-cbor@9.2.5':
2490
-
dependencies:
2491
-
cborg: 4.2.15
2492
-
multiformats: 13.4.1
2493
-
2494
2475
'@isaacs/cliui@8.0.2':
2495
2476
dependencies:
2496
2477
string-width: 5.1.2
···
3067
3048
get-intrinsic: 1.3.0
3068
3049
3069
3050
camelcase@6.3.0: {}
3070
-
3071
-
cborg@4.2.15: {}
3072
3051
3073
3052
chai@5.3.3:
3074
3053
dependencies:
···
3804
3783
yargs-unparser: 2.0.0
3805
3784
3806
3785
ms@2.1.3: {}
3807
-
3808
-
multiformats@13.4.1: {}
3809
3786
3810
3787
multiformats@9.9.0: {}
3811
3788
+5
-10
src/lib/notary.ts
+5
-10
src/lib/notary.ts
···
4
4
const { EAS, SchemaEncoder, SchemaRegistry, NO_EXPIRATION } = EASPackage;
5
5
6
6
import type { NotaryConfig, NotarizationResult, AttestationData } from './types';
7
-
import { parseRecordURI, hashContent, getExplorerURL, extractHashFromCID } from './utils';
7
+
import { parseRecordURI, hashContent, getExplorerURL } from './utils';
8
8
9
9
// Default schemas (deployed by atnotary maintainers)
10
10
const DEFAULT_SCHEMAS = {
···
277
277
currentHash?: string;
278
278
}> {
279
279
try {
280
+
// fetchRecord now returns { record, pds }
280
281
const { record } = await this.fetchRecord(attestationData.recordURI);
282
+
console.log(record.value)
283
+
const currentHash = hashContent(record.value);
281
284
const currentCid = record.cid;
282
285
283
-
// Compute hash using DAG-CBOR (same as CID)
284
-
const currentHash = hashContent(record.value);
285
-
286
-
// Extract hash from CID for comparison
287
-
const cidHash = extractHashFromCID(currentCid);
288
-
289
286
return {
290
287
exists: true,
291
288
cidMatches: currentCid === attestationData.cid,
292
-
// Now contentHash should match the hash inside CID!
293
-
hashMatches: currentHash === attestationData.contentHash && currentHash === cidHash,
289
+
hashMatches: currentHash === attestationData.contentHash,
294
290
currentCid,
295
291
currentHash,
296
292
};
···
305
301
throw error;
306
302
}
307
303
}
308
-
309
304
310
305
/**
311
306
* Get signer address
+7
-27
src/lib/utils.ts
+7
-27
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:[^\/]+)\/([^\/]+)\/(.+)$/);
···
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
-
}
+17
-43
tests/integration/notary.test.ts
+17
-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);
···
225
197
226
198
const attestation = await notary.verifyAttestation('0xattestationuid123');
227
199
const comparison = await notary.compareWithCurrent(attestation);
200
+
console.log({ attestation, comparison })
228
201
229
202
expect(comparison.exists).toBe(true);
230
203
expect(comparison.cidMatches).toBe(true);
231
204
expect(comparison.hashMatches).toBe(true);
232
-
expect(comparison.currentCid).toBe(TEST_CID_1);
233
-
expect(comparison.currentHash).toBe(TEST_HASH_1);
205
+
expect(comparison.currentCid).toBeTruthy();
206
+
expect(comparison.currentHash).toBeTruthy();
234
207
});
235
208
236
209
it('should detect changed content', async () => {
237
-
// Mock different content
210
+
// Mock different CID and hash
238
211
const { AtpAgent } = await import('@atproto/api');
239
-
(AtpAgent as any).mockImplementationOnce(() => ({
212
+
(AtpAgent as any).mockImplementation(() => ({
240
213
com: {
241
214
atproto: {
242
215
repo: {
243
216
getRecord: vi.fn().mockResolvedValue({
244
217
data: {
245
-
value: TEST_CONTENT_2,
246
-
cid: TEST_CID_2,
218
+
value: { text: 'Modified post', createdAt: '2024-01-02' },
219
+
cid: 'bafyreidifferent123',
247
220
},
248
221
}),
249
222
},
···
260
233
261
234
expect(comparison.exists).toBe(true);
262
235
expect(comparison.cidMatches).toBe(false);
263
-
expect(comparison.currentCid).toBe(TEST_CID_2);
236
+
expect(comparison.hashMatches).toBe(false);
264
237
});
265
238
});
239
+
266
240
});
+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