Skip to content

Commit 24d552d

Browse files
committed
refactor: schema v4
changes autoconfig.json format to one from ipshipyard/conf.ipfs-mainnet.org#4 at v4
1 parent 273e18e commit 24d552d

34 files changed

+1526
-442
lines changed

boxo/autoconfig/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const (
4040
// This is a specific version that is known to work fine with current implementation,
4141
// and it makes it a safe default while iterating on format.
4242
// TODO: change it back to https://config.ipfs-mainnet.org/autoconfig.json before shipping
43-
MainnetAutoConfigURL = "https://github.com/ipshipyard/config.ipfs-mainnet.org/raw/8fc9d8a793d13922be0fc5ea0634162613eadf6f/autoconfig.json"
43+
MainnetAutoConfigURL = "https://github.com/ipshipyard/config.ipfs-mainnet.org/raw/753001eab598b722d0377bef41287502105da8f8/autoconfig.json"
4444
)
4545

4646
// Client is the autoconfig client

boxo/autoconfig/client_test.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,27 @@ func TestGetLatest(t *testing.T) {
5454
// Create test config
5555
testConfig := &Config{
5656
AutoConfigVersion: 2025071802,
57-
AutoConfigSchema: 2,
58-
Bootstrap: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWGzxzKZYveHXtpG6AsrUJBcWxHBFS2HsEoGTxrMLvKXtf"},
59-
DNSResolvers: map[string][]string{"eth.": {"https://example.com"}},
60-
DelegatedRouters: map[string]DelegatedRouterConfig{
61-
MainnetProfileNodesWithDHT: {"https://cid.contact/routing/v1/providers"},
57+
AutoConfigSchema: 4,
58+
CacheTTL: 86400,
59+
SystemRegistry: map[string]SystemConfig{
60+
SystemAminoDHT: {
61+
Description: "Test AminoDHT system",
62+
NativeConfig: &NativeConfig{
63+
Bootstrap: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWGzxzKZYveHXtpG6AsrUJBcWxHBFS2HsEoGTxrMLvKXtf"},
64+
},
65+
DelegatedConfig: &DelegatedConfig{
66+
Read: []string{"/routing/v1/providers"},
67+
Write: []string{},
68+
},
69+
},
6270
},
63-
DelegatedPublishers: map[string]DelegatedPublisherConfig{
64-
MainnetProfileIPNSPublishers: {"https://delegated-ipfs.dev/routing/v1/ipns"},
71+
DNSResolvers: map[string][]string{"eth.": {"https://example.com"}},
72+
DelegatedEndpoints: map[string]EndpointConfig{
73+
"https://ipni.example.com": {
74+
Systems: []string{SystemIPNI},
75+
Read: []string{"/routing/v1/providers"},
76+
Write: []string{},
77+
},
6578
},
6679
}
6780

boxo/autoconfig/fallbacks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ var (
3030
// FallbackDelegatedRouters are the default delegated routing endpoints from Kubo 0.36
3131
// Used as last-resort fallback when autoconfig fetch fails
3232
FallbackDelegatedRouters = []string{
33-
"https://cid.contact/routing/v1/providers",
33+
"https://ipni.example.com/routing/v1/providers",
3434
}
3535

3636
// FallbackDelegatedPublishers are the default delegated IPNS publishers matching mainnet autoconfig

boxo/autoconfig/fetch.go

Lines changed: 150 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import (
1818
)
1919

2020
// GetLatest fetches the latest config with metadata, using cache when possible
21-
// The refreshInterval parameter determines how long cached configs are considered fresh
21+
// The refreshInterval parameter determines how long cached configs are considered fresh.
22+
// The effective refresh interval will be the minimum of refreshInterval and the server's CacheTTL.
2223
func (c *Client) GetLatest(ctx context.Context, configURL string, refreshInterval time.Duration) (*Response, error) {
2324
cacheDir, err := c.getCacheDir(configURL)
2425
if err != nil {
@@ -54,6 +55,26 @@ func (c *Client) GetLatest(ctx context.Context, configURL string, refreshInterva
5455
return nil, fmt.Errorf("failed to fetch from remote (%w) and no valid cache available (%w)", err, cacheErr)
5556
}
5657

58+
// Successfully fetched new config, now check if server CacheTTL requires using cached version
59+
if resp.Config != nil && cacheErr == nil {
60+
effectiveInterval := calculateEffectiveRefreshInterval(refreshInterval, resp.Config.CacheTTL)
61+
if effectiveInterval < refreshInterval {
62+
log.Debugf("server CacheTTL (%ds) is shorter than user refresh interval (%s), using effective interval %s",
63+
resp.Config.CacheTTL, refreshInterval, effectiveInterval)
64+
65+
// Re-check if cached version is still fresh under the effective (shorter) interval
66+
if cachedResp.CacheAge < effectiveInterval {
67+
log.Debugf("cached config is still fresh under server CacheTTL (%s)", effectiveInterval)
68+
return cachedResp, nil
69+
}
70+
// If cached version is also stale under server TTL, continue with newly fetched config
71+
log.Debugf("cached config is stale even under server CacheTTL, using newly fetched config")
72+
} else {
73+
log.Debugf("using user refresh interval (%s), server CacheTTL (%ds) is longer or not specified",
74+
refreshInterval, resp.Config.CacheTTL)
75+
}
76+
}
77+
5778
// Clean up old versions
5879
if err := c.cleanupOldVersions(cacheDir); err != nil {
5980
log.Warnf("failed to cleanup old versions: %v", err)
@@ -75,48 +96,101 @@ func (c *Client) GetCached(cacheDir string) (*Response, error) {
7596
// MustGetConfig returns config with fallbacks to hardcoded defaults
7697
// For cache-only behavior, pass a cancelled context
7798
// This method never returns an error and always returns usable mainnet values
99+
// The effective refresh interval will be the minimum of refreshInterval and the server's CacheTTL
78100
func (c *Client) MustGetConfig(ctx context.Context, configURL string, refreshInterval time.Duration) *Config {
79101
resp, err := c.GetLatest(ctx, configURL, refreshInterval)
80102
var config *Config
81103
if err == nil {
82104
config = resp.Config
83105
}
84106
if err != nil {
85-
// Return fallback config
107+
// Return fallback config with new structure
86108
return &Config{
87-
Bootstrap: FallbackBootstrapPeers,
88-
DNSResolvers: FallbackDNSResolvers,
89-
DelegatedRouters: map[string]DelegatedRouterConfig{
90-
MainnetProfileNodesWithDHT: DelegatedRouterConfig(FallbackDelegatedRouters),
91-
MainnetProfileNodesWithoutDHT: DelegatedRouterConfig(FallbackDelegatedRouters),
109+
AutoConfigVersion: 0, // Indicates fallback config
110+
AutoConfigSchema: 4,
111+
SystemRegistry: map[string]SystemConfig{
112+
SystemAminoDHT: {
113+
Description: "Fallback AminoDHT configuration",
114+
NativeConfig: &NativeConfig{
115+
Bootstrap: FallbackBootstrapPeers,
116+
},
117+
DelegatedConfig: &DelegatedConfig{
118+
Read: []string{"/routing/v1/providers", "/routing/v1/peers", "/routing/v1/ipns"},
119+
Write: []string{"/routing/v1/ipns"},
120+
},
121+
},
122+
SystemIPNI: {
123+
Description: "Fallback IPNI configuration",
124+
DelegatedConfig: &DelegatedConfig{
125+
Read: []string{"/routing/v1/providers"},
126+
Write: []string{},
127+
},
128+
},
92129
},
93-
DelegatedPublishers: map[string]DelegatedPublisherConfig{
94-
MainnetProfileIPNSPublishers: DelegatedPublisherConfig(FallbackDelegatedPublishers),
130+
DNSResolvers: FallbackDNSResolvers,
131+
DelegatedEndpoints: map[string]EndpointConfig{
132+
"https://ipni.example.com": {
133+
Systems: []string{SystemIPNI},
134+
Read: []string{"/routing/v1/providers"},
135+
Write: []string{},
136+
},
95137
},
96138
}
97139
}
98140

99-
// Fill in missing fields with fallbacks
100-
if len(config.Bootstrap) == 0 {
101-
config.Bootstrap = FallbackBootstrapPeers
141+
// Fill in missing fields with fallbacks for new structure
142+
if config.SystemRegistry == nil {
143+
config.SystemRegistry = make(map[string]SystemConfig)
102144
}
103145
if len(config.DNSResolvers) == 0 {
104146
config.DNSResolvers = FallbackDNSResolvers
105147
}
106-
if config.DelegatedRouters == nil {
107-
config.DelegatedRouters = make(map[string]DelegatedRouterConfig)
108-
}
109-
if len(config.DelegatedRouters[MainnetProfileNodesWithDHT]) == 0 {
110-
config.DelegatedRouters[MainnetProfileNodesWithDHT] = DelegatedRouterConfig(FallbackDelegatedRouters)
148+
if config.DelegatedEndpoints == nil {
149+
config.DelegatedEndpoints = make(map[string]EndpointConfig)
111150
}
112-
if len(config.DelegatedRouters[MainnetProfileNodesWithoutDHT]) == 0 {
113-
config.DelegatedRouters[MainnetProfileNodesWithoutDHT] = DelegatedRouterConfig(FallbackDelegatedRouters)
151+
152+
// Ensure AminoDHT system exists with fallback bootstrap
153+
if _, exists := config.SystemRegistry[SystemAminoDHT]; !exists {
154+
config.SystemRegistry[SystemAminoDHT] = SystemConfig{
155+
Description: "Fallback AminoDHT configuration",
156+
NativeConfig: &NativeConfig{
157+
Bootstrap: FallbackBootstrapPeers,
158+
},
159+
DelegatedConfig: &DelegatedConfig{
160+
Read: []string{"/routing/v1/providers", "/routing/v1/peers", "/routing/v1/ipns"},
161+
Write: []string{"/routing/v1/ipns"},
162+
},
163+
}
164+
} else if config.SystemRegistry[SystemAminoDHT].NativeConfig == nil || len(config.SystemRegistry[SystemAminoDHT].NativeConfig.Bootstrap) == 0 {
165+
// Update existing system with fallback bootstrap if missing
166+
system := config.SystemRegistry[SystemAminoDHT]
167+
if system.NativeConfig == nil {
168+
system.NativeConfig = &NativeConfig{}
169+
}
170+
if len(system.NativeConfig.Bootstrap) == 0 {
171+
system.NativeConfig.Bootstrap = FallbackBootstrapPeers
172+
}
173+
config.SystemRegistry[SystemAminoDHT] = system
114174
}
115-
if config.DelegatedPublishers == nil {
116-
config.DelegatedPublishers = make(map[string]DelegatedPublisherConfig)
175+
176+
// Ensure IPNI system exists
177+
if _, exists := config.SystemRegistry[SystemIPNI]; !exists {
178+
config.SystemRegistry[SystemIPNI] = SystemConfig{
179+
Description: "Fallback IPNI configuration",
180+
DelegatedConfig: &DelegatedConfig{
181+
Read: []string{"/routing/v1/providers"},
182+
Write: []string{},
183+
},
184+
}
117185
}
118-
if len(config.DelegatedPublishers[MainnetProfileIPNSPublishers]) == 0 {
119-
config.DelegatedPublishers[MainnetProfileIPNSPublishers] = DelegatedPublisherConfig(FallbackDelegatedPublishers)
186+
187+
// Ensure at least one delegated endpoint exists
188+
if len(config.DelegatedEndpoints) == 0 {
189+
config.DelegatedEndpoints["https://ipni.example.com"] = EndpointConfig{
190+
Systems: []string{SystemIPNI},
191+
Read: []string{"/routing/v1/providers"},
192+
Write: []string{},
193+
}
120194
}
121195

122196
return config
@@ -432,10 +506,14 @@ func formatDuration(d time.Duration) string {
432506

433507
// validateConfig validates all multiaddr and URL values in the config
434508
func (c *Client) validateConfig(config *Config) error {
435-
// Validate Bootstrap multiaddrs
436-
for i, bootstrap := range config.Bootstrap {
437-
if _, err := ma.NewMultiaddr(bootstrap); err != nil {
438-
return fmt.Errorf("Bootstrap[%d] invalid multiaddr %q: %w", i, bootstrap, err)
509+
// Validate SystemRegistry bootstrap multiaddrs
510+
for systemName, system := range config.SystemRegistry {
511+
if system.NativeConfig != nil {
512+
for i, bootstrap := range system.NativeConfig.Bootstrap {
513+
if _, err := ma.NewMultiaddr(bootstrap); err != nil {
514+
return fmt.Errorf("SystemRegistry[%q].NativeConfig.Bootstrap[%d] invalid multiaddr %q: %w", systemName, i, bootstrap, err)
515+
}
516+
}
439517
}
440518
}
441519

@@ -448,23 +526,57 @@ func (c *Client) validateConfig(config *Config) error {
448526
}
449527
}
450528

451-
// Validate DelegatedRouters URLs
452-
for routerType, routerConfig := range config.DelegatedRouters {
453-
for i, urlStr := range routerConfig {
454-
if _, err := url.Parse(urlStr); err != nil {
455-
return fmt.Errorf("DelegatedRouters[%q][%d] invalid URL %q: %w", routerType, i, urlStr, err)
529+
// Validate DelegatedEndpoints URLs (must be absolute HTTP/HTTPS URLs)
530+
for endpointURL, endpointConfig := range config.DelegatedEndpoints {
531+
parsed, err := url.Parse(endpointURL)
532+
if err != nil {
533+
return fmt.Errorf("DelegatedEndpoints URL %q invalid: %w", endpointURL, err)
534+
}
535+
536+
// Require absolute URLs with HTTP/HTTPS scheme
537+
if parsed.Scheme == "" {
538+
return fmt.Errorf("DelegatedEndpoints URL %q must be absolute (missing scheme)", endpointURL)
539+
}
540+
if parsed.Host == "" {
541+
return fmt.Errorf("DelegatedEndpoints URL %q must have a host", endpointURL)
542+
}
543+
if parsed.Scheme != "http" && parsed.Scheme != "https" {
544+
return fmt.Errorf("DelegatedEndpoints URL %q must use http or https scheme, got %q", endpointURL, parsed.Scheme)
545+
}
546+
547+
// Validate Read paths
548+
for i, path := range endpointConfig.Read {
549+
if !strings.HasPrefix(path, "/") {
550+
return fmt.Errorf("DelegatedEndpoints[%q].Read[%d] path %q must start with /", endpointURL, i, path)
456551
}
457552
}
458-
}
459553

460-
// Validate DelegatedPublishers URLs
461-
for publisherType, publisherConfig := range config.DelegatedPublishers {
462-
for i, urlStr := range publisherConfig {
463-
if _, err := url.Parse(urlStr); err != nil {
464-
return fmt.Errorf("DelegatedPublishers[%q][%d] invalid URL %q: %w", publisherType, i, urlStr, err)
554+
// Validate Write paths
555+
for i, path := range endpointConfig.Write {
556+
if !strings.HasPrefix(path, "/") {
557+
return fmt.Errorf("DelegatedEndpoints[%q].Write[%d] path %q must start with /", endpointURL, i, path)
465558
}
466559
}
467560
}
468561

469562
return nil
470563
}
564+
565+
// calculateEffectiveRefreshInterval returns the minimum of user-provided interval and server CacheTTL.
566+
// This ensures that both user preferences and server cache policies are respected.
567+
// If cacheTTLSeconds is 0 or negative, only the user interval is used.
568+
func calculateEffectiveRefreshInterval(userInterval time.Duration, cacheTTLSeconds int) time.Duration {
569+
if cacheTTLSeconds <= 0 {
570+
// Server doesn't specify TTL or specifies invalid TTL, use user preference
571+
return userInterval
572+
}
573+
574+
serverTTL := time.Duration(cacheTTLSeconds) * time.Second
575+
if serverTTL < userInterval {
576+
// Server wants shorter cache period, respect it
577+
return serverTTL
578+
}
579+
580+
// User wants shorter cache period or same as server, respect user preference
581+
return userInterval
582+
}

0 commit comments

Comments
 (0)