Repair swarm bootstrap seed for new and existing nodes
Hop-State: A_06FP90SDWE5709BB1G6J4B8 Hop-Proposal: R_06FP90RS27BNBV5H5N053A0 Hop-Task: T_06FP905Q50D1ZFPC56N3DZG Hop-Attempt: AT_06FP905Q50E6P72ZMVT4038
This commit is contained in:
@@ -8,7 +8,11 @@ import { db, nodes, users, posts } from '@/db';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import type { SwarmAnnouncement, SwarmNodeInfo, SwarmCapability } from './types';
|
||||
import { upsertSwarmNode, getSeedNodes, markNodeSuccess, markNodeFailure } from './registry';
|
||||
import { getPublicSwarmDomain, isPublicSwarmDomain } from './node-domain';
|
||||
import {
|
||||
getCanonicalSwarmSeedDomain,
|
||||
getPublicSwarmDomain,
|
||||
isPublicSwarmDomain,
|
||||
} from './node-domain';
|
||||
|
||||
const PUBLIC_SWARM_DOMAIN_ERROR = 'Public swarm participation requires a real ICANN domain';
|
||||
|
||||
@@ -84,7 +88,7 @@ export async function announceToNode(targetDomain: string): Promise<{ success: b
|
||||
return { success: false, error: PUBLIC_SWARM_DOMAIN_ERROR };
|
||||
}
|
||||
|
||||
const publicTargetDomain = getPublicSwarmDomain(targetDomain);
|
||||
const publicTargetDomain = getCanonicalSwarmSeedDomain(targetDomain);
|
||||
if (!publicTargetDomain) {
|
||||
return { success: false, error: `Invalid public swarm domain: ${targetDomain}` };
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* The Synapsis swarm is a decentralized network of nodes that discover
|
||||
* and communicate with each other using a hybrid approach:
|
||||
*
|
||||
* 1. Seed nodes (like node.synapsis.social) provide initial bootstrap
|
||||
* 1. Seed nodes (like synapsis.social) provide initial bootstrap
|
||||
* 2. Gossip protocol spreads node/handle information epidemically
|
||||
* 3. Any node can discover the full network without central authority
|
||||
* 4. Direct node-to-node interactions (likes, follows, etc.) for instant delivery
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getPublicSwarmDomain, isPublicSwarmDomain } from './node-domain';
|
||||
import {
|
||||
getCanonicalSwarmSeedDomain,
|
||||
getPublicSwarmDomain,
|
||||
isPublicSwarmDomain,
|
||||
} from './node-domain';
|
||||
|
||||
describe('public swarm domains', () => {
|
||||
it.each([
|
||||
@@ -31,3 +35,17 @@ describe('public swarm domains', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('swarm seed domains', () => {
|
||||
it('maps the retired bootstrap hostname to the official node identity', () => {
|
||||
expect(getCanonicalSwarmSeedDomain('node.synapsis.social')).toBe('synapsis.social');
|
||||
expect(getCanonicalSwarmSeedDomain('https://NODE.SYNAPSIS.SOCIAL/')).toBe('synapsis.social');
|
||||
});
|
||||
|
||||
it('leaves other valid seed domains unchanged', () => {
|
||||
expect(getCanonicalSwarmSeedDomain('batorbros.bond')).toBe('batorbros.bond');
|
||||
});
|
||||
|
||||
it('rejects non-public seed domains', () => {
|
||||
expect(getCanonicalSwarmSeedDomain('localhost:43821')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { parse } from 'tldts';
|
||||
|
||||
const LEGACY_SWARM_SEED_DOMAINS: Readonly<Record<string, string>> = {
|
||||
'node.synapsis.social': 'synapsis.social',
|
||||
};
|
||||
|
||||
export function normalizeNodeDomain(domain: string): string {
|
||||
return domain
|
||||
.trim()
|
||||
@@ -52,3 +56,14 @@ export function isPublicSwarmDomain(value: string | null | undefined): boolean {
|
||||
return getPublicSwarmDomain(value) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve retired bootstrap hostnames to the node identity they represent.
|
||||
* This keeps upgraded nodes working even before their persisted seed rows are
|
||||
* rewritten by the accompanying data migration.
|
||||
*/
|
||||
export function getCanonicalSwarmSeedDomain(value: string | null | undefined): string | null {
|
||||
const domain = getPublicSwarmDomain(value);
|
||||
if (!domain) return null;
|
||||
|
||||
return LEGACY_SWARM_SEED_DOMAINS[domain] ?? domain;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@ import { db, swarmNodes, swarmSeeds, swarmSyncLog } from '@/db';
|
||||
import { eq, desc, and, gt, lt, sql } from 'drizzle-orm';
|
||||
import type { SwarmNodeInfo, SwarmCapability, SwarmSyncResult } from './types';
|
||||
import { SWARM_CONFIG, DEFAULT_SEED_NODES } from './types';
|
||||
import { getPublicSwarmDomain, isPublicSwarmDomain } from './node-domain';
|
||||
import {
|
||||
getCanonicalSwarmSeedDomain,
|
||||
getPublicSwarmDomain,
|
||||
isPublicSwarmDomain,
|
||||
} from './node-domain';
|
||||
|
||||
/**
|
||||
* Get or create a swarm node entry
|
||||
@@ -273,7 +277,11 @@ export async function getSeedNodes(): Promise<string[]> {
|
||||
orderBy: (swarmSeeds) => [swarmSeeds.priority],
|
||||
});
|
||||
|
||||
const publicSeeds = seeds.map(s => s.domain).filter(isPublicSwarmDomain);
|
||||
const publicSeeds = Array.from(new Set(
|
||||
seeds
|
||||
.map((seed) => getCanonicalSwarmSeedDomain(seed.domain))
|
||||
.filter((domain): domain is string => domain !== null)
|
||||
));
|
||||
|
||||
return publicSeeds.length > 0
|
||||
? publicSeeds
|
||||
@@ -286,7 +294,7 @@ export async function getSeedNodes(): Promise<string[]> {
|
||||
export async function addSeedNode(domain: string, priority = 100): Promise<void> {
|
||||
if (!db) return;
|
||||
|
||||
const normalizedDomain = getPublicSwarmDomain(domain);
|
||||
const normalizedDomain = getCanonicalSwarmSeedDomain(domain);
|
||||
if (!normalizedDomain) {
|
||||
throw new Error(`Seed nodes must use a public ICANN domain: ${domain}`);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export interface SwarmStats {
|
||||
|
||||
// Default seed nodes for bootstrapping
|
||||
export const DEFAULT_SEED_NODES = [
|
||||
'node.synapsis.social',
|
||||
'synapsis.social',
|
||||
] as const;
|
||||
|
||||
// Swarm configuration
|
||||
|
||||
Reference in New Issue
Block a user