import type { IPNSPublishTarget } from '@wovin/core/ipns' import { Logger } from 'besonders-logger' import { base64pad } from 'multiformats/bases/base64' import { CID } from 'multiformats/cid' import * as W3Name from 'w3name' const { WARN, LOG, DEBUG, ERROR } = Logger.setup(Logger.INFO) /** * Try to resolve IPNS name to get existing revision. * Returns null if record doesn't exist (404). * Throws on network errors or server errors. * * This does a custom HTTP check first to distinguish 404 from network errors, * then delegates to W3Name.resolve() for validation if record exists. */ async function tryResolveIPNS(ipns: W3Name.WritableName): Promise { const url = `https://name.web3.storage/name/${ipns.toString()}` let response: Response try { response = await fetch(url, { signal: AbortSignal.timeout(30_000) }) } catch (err) { // Network error (no connection, DNS failure, etc.) throw ERROR('[w3name] Network error resolving IPNS:', err) } // 404 = record never published if (response.status === 404) { DEBUG('[w3name] IPNS record not found (never published):', ipns.toString()) return null } // Other HTTP errors (5xx server error, etc.) if (!response.ok) { throw ERROR(`[w3name] HTTP ${response.status} resolving IPNS:`, response.statusText) } // Success - use W3Name.resolve to get validated Revision // (We could parse the record ourselves, but W3Name does validation/signature checks) const existing = await W3Name.resolve(ipns) return existing } /** * Publish CID to IPNS, automatically handling increment vs v0. * Returns the revision for further processing (e.g., Kubo integration). */ export async function publishIPNS(ipnsPrivateKey: Uint8Array, cid: CID): Promise { const TIMEOUT_MS = 30_000 const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`publishIPNS timed out after ${TIMEOUT_MS}ms`)), TIMEOUT_MS), ) return Promise.race([_publishIPNSImpl(ipnsPrivateKey, cid), timeout]) } async function _publishIPNSImpl(ipnsPrivateKey: Uint8Array, cid: CID): Promise { const value = `/ipfs/${cid}` const ipns = await W3Name.from(ipnsPrivateKey) let revision: W3Name.Revision const existing = await tryResolveIPNS(ipns) if (existing) { // Record exists - increment sequence number revision = await W3Name.increment(existing, value) DEBUG('[w3name] incrementing revision for', ipns.toString()) } else { // First publish - use v0 revision = await W3Name.v0(ipns, value) DEBUG('[w3name] creating initial revision for', ipns.toString()) } await W3Name.publish(revision, ipns.key) DEBUG('[w3name] published', cid.toString(), 'to', ipns.toString()) return revision // Return for Kubo integration or other uses } /** * Create an IPNSPublishTarget that publishes to W3Name service via HTTP POST. */ export function w3nameTarget(serviceUrl = 'https://name.web3.storage'): IPNSPublishTarget { return { name: 'w3name', async publish(ipnsName: string, recordBytes: Uint8Array) { const res = await fetch(`${serviceUrl}/name/${ipnsName}`, { method: 'POST', body: base64pad.baseEncode(recordBytes), }) if (!res.ok) throw new Error(`W3Name HTTP ${res.status}`) }, } } export async function generateIpnsKey() { return W3Name.create() // Returns W3Name.WritableName type } export async function getW3NamePublic(pk: Uint8Array) { const ipns = await W3Name.from(pk) return ipns.toString() }