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:
2026-07-14 23:42:26 -07:00
committed by Hop
parent 65e901393b
commit 6edf8991fa
7 changed files with 65 additions and 8 deletions
@@ -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';
+6 -2
View File
@@ -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}` };
}
+1 -1
View File
@@ -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
+19 -1
View File
@@ -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();
});
});
+15
View File
@@ -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;
}
+11 -3
View File
@@ -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}`);
}
+1 -1
View File
@@ -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