Checkpoint peering identity and opt-in discovery

This commit is contained in:
Codex committed 2026-10-13 22:00:54 +02:00
1 parent eba9b50e30
commit 47de081721
44 files changed
+1557 -55

No files matched your search

+70 -1
View File
@@ -71,7 +71,7 @@ test('mTLS pairing, opt-in catalog, peer queue publication, and revocation', { t
await fsp.writeFile(path.join(chapterDir, 'chapter.json'), JSON.stringify({ title: 'The first chapter', source: { name: 'Local' } }));
await fsp.writeFile(path.join(chapterDir, '0001.png'), PNG);
const ga = await gateway(dataA, libA, portA, path.join(root, 'usage-a.json'));
const gb = await gateway(dataB, libB, portB, path.join(root, 'usage-b.json'));
let gb = await gateway(dataB, libB, portB, path.join(root, 'usage-b.json'));
t.after(async () => { await stop(ga); await stop(gb); });
await client.probe(b, inB.id);
await client.probe(a, inA.id);
@@ -90,6 +90,7 @@ test('mTLS pairing, opt-in catalog, peer queue publication, and revocation', { t
assert.equal(listing.length, 1);
assert.equal(listing[0].title, 'Sample Manga');
assert.equal(listing[0].coverAvailable, true);
assert.deepEqual(listing[0].workIdentity, require('../../scraper/lib/work-identity').resolveWorkIdentity({}, 'Sample Manga', a.identity().id));
assert.deepEqual(listing[0].tags, []);
assert.equal(JSON.stringify(listing).includes('Sample Manga\\v01'), false);
const remote = await client.manga(b, inB.id, listing[0].id);
@@ -131,10 +132,78 @@ test('mTLS pairing, opt-in catalog, peer queue publication, and revocation', { t
const result = await run(info, { scope: { mode: 'missing' }, existingPolicy: 'skip-any' },
{ libraryRoot: libB, settings: { chapterMinMs: 500 }, shouldStop: () => false });
assert.equal(result.publishedCount, 1);
assert.deepEqual(JSON.parse(await fsp.readFile(path.join(libB, 'Sample Manga', 'manga.json'), 'utf8'))['x-work-identity'], remote.workIdentity,
'The stable work identity is stored with the imported peer edition');
assert.equal(fs.existsSync(path.join(libB, 'Sample Manga', 'v01', 'c0001 The first chapter [Peer ServerA ' + inB.id.slice(1, 7) + ']', '0001.png')), true);
const second = await run(info, { scope: { mode: 'missing' }, existingPolicy: 'skip-any' },
{ libraryRoot: libB, settings: { chapterMinMs: 500 }, shouldStop: () => false });
assert.equal(second.publishedCount, 0);
// Simulate B adding one chapter to its imported edition, then sharing it back.
// A must identify its original folder by the carried origin identity and fetch
// only the new chapter, rather than creating a title-conflict quarantine.
const chapterTwo = path.join(libB, 'Sample Manga', 'v01', 'c0002 Chapter from B');
await fsp.mkdir(chapterTwo, { recursive: true });
await fsp.writeFile(path.join(chapterTwo, 'chapter.json'), JSON.stringify({ title: 'Chapter from B', source: { name: 'Local' } }));
await fsp.writeFile(path.join(chapterTwo, '0001.png'), PNG);
b.setShare(inB.id, 'Sample Manga', true);
b.updatePeer(inB.id, { serve: true });
const reverseGrant = b.issueGrant(inB.id, ['download']);
a.acceptGrant(inA.id, reverseGrant.token);
a.updatePeer(inA.id, { receive: true });
const reverseCatalog = await client.catalog(a, inA.id);
assert.equal(reverseCatalog.length, 1);
const reverseManga = await client.manga(a, inA.id, reverseCatalog[0].id);
assert.deepEqual(reverseManga.workIdentity, remote.workIdentity, 'The original work identity survives a peer round trip');
process.env.PEER_DATA = dataA;
const reverseInfo = require('../adapter').infoFor(`peer://${inA.id}/${reverseCatalog[0].id}`);
const returned = await require('../adapter').run(reverseInfo, { scope: { mode: 'missing' }, existingPolicy: 'skip-any', workIdentity: reverseManga.workIdentity },
{ libraryRoot: libA, settings: { chapterMinMs: 500 }, shouldStop: () => false });
assert.equal(returned.finalPath, path.join(libA, 'Sample Manga'), 'The round-trip publication targets the original library folder');
assert.equal(returned.selectedCount, 1, 'Only the chapter added on B is selected for sync-missing');
assert.equal(returned.publishedCount, 1);
assert.equal(fs.existsSync(path.join(libA, 'Sample Manga', 'v01', 'c0002 Chapter from B [Peer ServerB ' + inA.id.slice(1, 7) + ']', '0001.png')), true);
const noChanges = await require('../adapter').run(reverseInfo, { scope: { mode: 'missing' }, existingPolicy: 'skip-any', workIdentity: reverseManga.workIdentity },
{ libraryRoot: libA, settings: { chapterMinMs: 500 }, shouldStop: () => false });
assert.equal(noChanges.selectedCount, 0, 'A repeated round trip queues no duplicate chapters');
// Simulate an older B copy which predates persisted identities. Exact shared
// page hashes may bridge that legacy gap, but title similarity alone may not.
const legacyMetadataPath = path.join(libB, 'Sample Manga', 'manga.json');
const legacyMetadata = JSON.parse(await fsp.readFile(legacyMetadataPath, 'utf8'));
delete legacyMetadata['x-work-identity'];
await fsp.writeFile(legacyMetadataPath, JSON.stringify(legacyMetadata));
const chapterThree = path.join(libB, 'Sample Manga', 'v01', 'c0003 Chapter from B');
await fsp.mkdir(chapterThree, { recursive: true });
await fsp.writeFile(path.join(chapterThree, 'chapter.json'), JSON.stringify({ title: 'Chapter from B', source: { name: 'Local' } }));
await fsp.writeFile(path.join(chapterThree, '0001.png'), PNG);
await stop(gb); // Clear the gateway's short-lived manifest cache after this legacy-data migration.
gb = await gateway(dataB, libB, portB, path.join(root, 'usage-b.json'));
const legacyManga = await client.manga(a, inA.id, reverseCatalog[0].id);
assert.notDeepEqual(legacyManga.workIdentity, remote.workIdentity, 'Legacy copy has no propagated identity and gets a peer-local fallback');
const legacySync = await require('../adapter').run(reverseInfo, { scope: { mode: 'missing' }, existingPolicy: 'skip-any', workIdentity: legacyManga.workIdentity },
{ libraryRoot: libA, settings: { chapterMinMs: 500 }, shouldStop: () => false });
assert.equal(legacySync.finalPath, path.join(libA, 'Sample Manga'), 'Content evidence safely reuses the original folder for legacy peer copies');
assert.equal(legacySync.selectedCount, 1, 'Legacy matching still selects only the chapter absent on A');
assert.equal(legacySync.publishedCount, 1);
assert.equal(fs.existsSync(path.join(libA, 'Sample Manga', 'v01', 'c0003 Chapter from B [Peer ServerB ' + inA.id.slice(1, 7) + ']', '0001.png')), true);
for (const book of await fsp.readdir(path.join(libB, 'Sample Manga'))) {
const bookPath = path.join(libB, 'Sample Manga', book);
if (!(await fsp.stat(bookPath)).isDirectory()) continue;
for (const chapter of await fsp.readdir(bookPath)) {
const page = path.join(bookPath, chapter, '0001.png');
if (fs.existsSync(page)) await fsp.writeFile(page, Buffer.from(`different-page-${chapter}`));
}
}
await stop(gb);
gb = await gateway(dataB, libB, portB, path.join(root, 'usage-b.json'));
const unverifiedLegacy = await client.manga(a, inA.id, reverseCatalog[0].id);
await assert.rejects(() => require('../adapter').run(reverseInfo,
{ scope: { mode: 'missing' }, existingPolicy: 'skip-any', workIdentity: unverifiedLegacy.workIdentity },
{ libraryRoot: libA, settings: { chapterMinMs: 500 }, shouldStop: () => false }),
/neither its canonical identity nor shared chapter page hashes verify.*No files were downloaded or changed/);
assert.equal((await require('../../scraper/lib/library-inventory').scanLibrary(libA)).mangas[0].chapters.length, 3,
'A title collision with differing chapter bytes leaves the local library untouched');
a.updatePeer(inA.id, { servingPaused: true });
assert.equal((await client.access(b, inB.id)).grant.status, 'paused');
await assert.rejects(() => client.catalog(b, inB.id), /Peer HTTP 403/);