From 6edf8991fa5252bb6cd6df53a6c0019c4aec89b7 Mon Sep 17 00:00:00 2001 From: cyph3rasi Date: Tue, 14 Jul 2026 23:42:26 -0700 Subject: [PATCH] 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 --- .../migration.sql | 12 +++++++++++ src/lib/swarm/discovery.ts | 8 ++++++-- src/lib/swarm/index.ts | 2 +- src/lib/swarm/node-domain.test.ts | 20 ++++++++++++++++++- src/lib/swarm/node-domain.ts | 15 ++++++++++++++ src/lib/swarm/registry.ts | 14 ++++++++++--- src/lib/swarm/types.ts | 2 +- 7 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 drizzle/20260715064500_fix_swarm_seed/migration.sql diff --git a/drizzle/20260715064500_fix_swarm_seed/migration.sql b/drizzle/20260715064500_fix_swarm_seed/migration.sql new file mode 100644 index 0000000..ba77d43 --- /dev/null +++ b/drizzle/20260715064500_fix_swarm_seed/migration.sql @@ -0,0 +1,12 @@ +DELETE FROM `swarm_seeds` +WHERE `domain` = 'node.synapsis.social' + AND EXISTS ( + SELECT 1 FROM `swarm_seeds` AS `canonical_seed` + WHERE `canonical_seed`.`domain` = 'synapsis.social' + ); +--> statement-breakpoint +UPDATE `swarm_seeds` +SET + `domain` = 'synapsis.social', + `consecutive_failures` = 0 +WHERE `domain` = 'node.synapsis.social'; diff --git a/src/lib/swarm/discovery.ts b/src/lib/swarm/discovery.ts index 95b6b58..52ea44e 100644 --- a/src/lib/swarm/discovery.ts +++ b/src/lib/swarm/discovery.ts @@ -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}` }; } diff --git a/src/lib/swarm/index.ts b/src/lib/swarm/index.ts index 08dec71..31cc9ec 100644 --- a/src/lib/swarm/index.ts +++ b/src/lib/swarm/index.ts @@ -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 diff --git a/src/lib/swarm/node-domain.test.ts b/src/lib/swarm/node-domain.test.ts index 15dd8a2..cf2edaa 100644 --- a/src/lib/swarm/node-domain.test.ts +++ b/src/lib/swarm/node-domain.test.ts @@ -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(); + }); +}); diff --git a/src/lib/swarm/node-domain.ts b/src/lib/swarm/node-domain.ts index 1eba667..ae87de7 100644 --- a/src/lib/swarm/node-domain.ts +++ b/src/lib/swarm/node-domain.ts @@ -1,5 +1,9 @@ import { parse } from 'tldts'; +const LEGACY_SWARM_SEED_DOMAINS: Readonly> = { + '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; +} diff --git a/src/lib/swarm/registry.ts b/src/lib/swarm/registry.ts index 4304b4f..0dc0341 100644 --- a/src/lib/swarm/registry.ts +++ b/src/lib/swarm/registry.ts @@ -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 { 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 { export async function addSeedNode(domain: string, priority = 100): Promise { 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}`); } diff --git a/src/lib/swarm/types.ts b/src/lib/swarm/types.ts index 5283c6c..f6e8d93 100644 --- a/src/lib/swarm/types.ts +++ b/src/lib/swarm/types.ts @@ -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