diff --git a/patches/@helia+block-brokers+3.0.0.patch b/patches/@helia+block-brokers+3.0.0.patch new file mode 100644 index 00000000..0ad0de04 --- /dev/null +++ b/patches/@helia+block-brokers+3.0.0.patch @@ -0,0 +1,24 @@ +diff --git a/node_modules/@helia/block-brokers/dist/src/trustless-gateway/broker.js b/node_modules/@helia/block-brokers/dist/src/trustless-gateway/broker.js +index 223f634..34ff22d 100644 +--- a/node_modules/@helia/block-brokers/dist/src/trustless-gateway/broker.js ++++ b/node_modules/@helia/block-brokers/dist/src/trustless-gateway/broker.js +@@ -58,10 +58,14 @@ export class TrustlessGatewayBlockBroker { + } + } + createSession(options = {}) { +- return createTrustlessGatewaySession({ +- logger: this.logger, +- routing: this.routing +- }, options); ++ return createTrustlessGatewaySession({ ++ logger: this.logger, ++ routing: this.routing ++ }, { ++ ...options, ++ allowLocal: this.allowLocal, ++ allowInsecure: this.allowInsecure ++ }) + } + } + //# sourceMappingURL=broker.js.map +\ No newline at end of file diff --git a/src/lib/config-db.ts b/src/lib/config-db.ts index a858c359..a7b585e1 100644 --- a/src/lib/config-db.ts +++ b/src/lib/config-db.ts @@ -8,6 +8,7 @@ export interface ConfigDb extends BaseDbConfig { routers: string[] dnsJsonResolvers: Record autoReload: boolean + delegatedRouting: boolean debug: string } @@ -16,6 +17,8 @@ export const defaultRouters = ['https://delegated-ipfs.dev'] export const defaultDnsJsonResolvers = { '.': 'https://delegated-ipfs.dev/dns-query' } +export const defaultdelegatedRouting = true +export const defaultAutoReload = false const configDb = new GenericIDB('helia-sw', 'config') @@ -26,7 +29,10 @@ export async function loadConfigFromLocalStorage (): Promise { const localStorageGatewaysString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) ?? JSON.stringify(defaultGateways) const localStorageRoutersString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.routers) ?? JSON.stringify(defaultRouters) const localStorageDnsResolvers = localStorage.getItem(LOCAL_STORAGE_KEYS.config.dnsJsonResolvers) ?? JSON.stringify(defaultDnsJsonResolvers) - const autoReload = localStorage.getItem(LOCAL_STORAGE_KEYS.config.autoReload) === 'true' + const lsDelegatedRouting = localStorage.getItem(LOCAL_STORAGE_KEYS.config.delegatedRouting) + const delegatedRouting = lsDelegatedRouting === null ? defaultAutoReload : lsDelegatedRouting === 'true' + const lsAutoReload = localStorage.getItem(LOCAL_STORAGE_KEYS.config.autoReload) + const autoReload = lsAutoReload === null ? defaultAutoReload : lsAutoReload === 'true' const debug = localStorage.getItem(LOCAL_STORAGE_KEYS.config.debug) ?? '' const gateways = JSON.parse(localStorageGatewaysString) const routers = JSON.parse(localStorageRoutersString) @@ -35,6 +41,7 @@ export async function loadConfigFromLocalStorage (): Promise { await configDb.put('gateways', gateways) await configDb.put('routers', routers) + await configDb.put('delegatedRouting', delegatedRouting) await configDb.put('dnsJsonResolvers', dnsJsonResolvers) await configDb.put('autoReload', autoReload) await configDb.put('debug', debug) @@ -44,15 +51,17 @@ export async function loadConfigFromLocalStorage (): Promise { export async function resetConfig (): Promise { await configDb.open() - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.gateways) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.gateways, JSON.stringify(defaultGateways)) await configDb.put('gateways', defaultGateways) - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.routers) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.routers, JSON.stringify(defaultRouters)) await configDb.put('routers', defaultRouters) - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.dnsJsonResolvers) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.delegatedRouting, String(defaultdelegatedRouting)) + await configDb.put('delegatedRouting', true) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.dnsJsonResolvers, JSON.stringify(defaultDnsJsonResolvers)) await configDb.put('dnsJsonResolvers', defaultDnsJsonResolvers) - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.autoReload) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.autoReload, String(defaultAutoReload)) await configDb.put('autoReload', false) - localStorage.removeItem(LOCAL_STORAGE_KEYS.config.debug) + localStorage.setItem(LOCAL_STORAGE_KEYS.config.debug, '') await configDb.put('debug', '') configDb.close() } @@ -65,6 +74,7 @@ export async function setConfig (config: ConfigDb, logger: ComponentLogger): Pro await configDb.open() await configDb.put('gateways', config.gateways) await configDb.put('routers', config.routers) + await configDb.put('delegatedRouting', config.delegatedRouting) await configDb.put('dnsJsonResolvers', config.dnsJsonResolvers) await configDb.put('autoReload', config.autoReload) await configDb.put('debug', config.debug ?? '') @@ -76,7 +86,8 @@ export async function getConfig (logger: ComponentLogger): Promise { let gateways: string[] = defaultGateways let routers: string[] = defaultRouters let dnsJsonResolvers: Record = defaultDnsJsonResolvers - let autoReload = false + let autoReload = defaultAutoReload + let delegatedRouting = defaultdelegatedRouting let debug = '' try { @@ -88,7 +99,10 @@ export async function getConfig (logger: ComponentLogger): Promise { dnsJsonResolvers = await configDb.get('dnsJsonResolvers') - autoReload = await configDb.get('autoReload') ?? false + autoReload = await configDb.get('autoReload') ?? defaultAutoReload + + delegatedRouting = await configDb.get('delegatedRouting') ?? defaultdelegatedRouting + debug = await configDb.get('debug') ?? '' configDb.close() debugLib.enable(debug) @@ -100,7 +114,7 @@ export async function getConfig (logger: ComponentLogger): Promise { gateways = [...defaultGateways] } - if (routers == null || routers.length === 0) { + if (routers == null) { routers = [...defaultRouters] } if (dnsJsonResolvers == null || Object.keys(dnsJsonResolvers).length === 0) { @@ -111,6 +125,7 @@ export async function getConfig (logger: ComponentLogger): Promise { return { gateways, routers, + delegatedRouting, dnsJsonResolvers, autoReload, debug diff --git a/src/lib/local-gateway.ts b/src/lib/local-gateway.ts new file mode 100644 index 00000000..1695001a --- /dev/null +++ b/src/lib/local-gateway.ts @@ -0,0 +1,33 @@ +import { uiLogger } from './logger.js' + +export const localGwUrl = 'http://localhost:8080' +// export const localGwUrl = 'http://localhost:8080' +const localGwTestUrl = `${localGwUrl}/ipfs/bafkqablimvwgy3y?format=raw` +const expectedContentType = 'application/vnd.ipld.raw' +const expectedResponseBody = 'hello' + +const log = uiLogger.forComponent('local-gateway-prober') + +export async function hasLocalGateway (): Promise { + try { + log(`probing for local trustless gateway at ${localGwTestUrl}`) + const resp = await fetch(localGwTestUrl, { cache: 'no-store' }) + if (!resp.ok) { + return false + } + if (resp.headers.get('Content-Type') !== expectedContentType) { + return false + } + const respBody = await resp.text() + + if (respBody === expectedResponseBody) { + log(`found local trustless gateway at ${localGwTestUrl}`) + return true + } else { + return false + } + } catch (e: unknown) { + log.error('failed to probe trustless gateway', e) + return false + } +} diff --git a/src/lib/local-storage.ts b/src/lib/local-storage.ts index 68c590a1..8f6c6588 100644 --- a/src/lib/local-storage.ts +++ b/src/lib/local-storage.ts @@ -9,6 +9,7 @@ export const LOCAL_STORAGE_KEYS = { gateways: getLocalStorageKey('config', 'gateways'), routers: getLocalStorageKey('config', 'routers'), autoReload: getLocalStorageKey('config', 'autoReload'), + delegatedRouting: getLocalStorageKey('config', 'delegatedRouting'), dnsJsonResolvers: getLocalStorageKey('config', 'dnsJsonResolvers'), debug: getLocalStorageKey('config', 'debug') }, diff --git a/src/pages/config.tsx b/src/pages/config.tsx index 4ffa1340..d18acd54 100644 --- a/src/pages/config.tsx +++ b/src/pages/config.tsx @@ -8,6 +8,7 @@ import { RouteContext } from '../context/router-context.jsx' import { ServiceWorkerProvider } from '../context/service-worker-context.jsx' import { HeliaServiceWorkerCommsChannel } from '../lib/channel.js' import { defaultDnsJsonResolvers, defaultGateways, defaultRouters, getConfig, loadConfigFromLocalStorage, resetConfig } from '../lib/config-db.js' +import { hasLocalGateway, localGwUrl } from '../lib/local-gateway.js' import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.js' import { getUiComponentLogger, uiLogger } from '../lib/logger.js' import './default-page-styles.css' @@ -16,7 +17,7 @@ const uiComponentLogger = getUiComponentLogger('config-page') const log = uiLogger.forComponent('config-page') const channel = new HeliaServiceWorkerCommsChannel('WINDOW', uiComponentLogger) -const urlValidationFn = (value: string): Error | null => { +const gatewayArrayValidationFn = (value: string): Error | null => { try { const urls = JSON.parse(value) satisfies string[] let i = 0 @@ -37,6 +38,24 @@ const urlValidationFn = (value: string): Error | null => { } } +const routersArrayValidationFn = (value: string): Error | null => { + try { + const urls = JSON.parse(value) satisfies string[] + let i = 0 + try { + urls.map((url, index) => { + i = index + return new URL(url) + }) + } catch (e) { + throw new Error(`URL "${urls[i]}" at index ${i} is not valid`) + } + return null + } catch (err) { + return err as Error + } +} + const dnsJsonValidationFn = (value: string): Error | null => { try { const urls: Record = JSON.parse(value) @@ -84,10 +103,41 @@ function ConfigPage (): React.JSX.Element | null { window.parent?.postMessage({ source: 'helia-sw-config-iframe', target: 'PARENT', action: 'RELOAD_CONFIG', config }, { targetOrigin }) - log.trace('config-page: RELOAD_CONFIG sent to parent window') + log.trace('RELOAD_CONFIG sent to parent window') }, []) + // Effect to add or remove the local gateway useEffect(() => { + hasLocalGateway() + .then(async hasLocalGw => { + // check if local storage has it. + const unparsedGwConf = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) + let gwConf = unparsedGwConf != null ? JSON.parse(unparsedGwConf) as string[] : defaultGateways + + if (hasLocalGw) { + // Add the local gateway to config if not there already + if (!gwConf.includes(localGwUrl)) { + log(`Adding ${localGwUrl} to gateway list`) + gwConf.unshift(localGwUrl) + } + } else if (gwConf.includes(localGwUrl)) { + // remove local gateway from the configuration if the gateway is not available + gwConf = gwConf.filter(gw => gw !== localGwUrl) + if (gwConf.length === 0) { + // if there are no gateways following the removal reset to the default gateways + gwConf = defaultGateways + } + } + + // persist to localstorage, idb and 🙃 + localStorage.setItem(LOCAL_STORAGE_KEYS.config.gateways, JSON.stringify(gwConf)) + await loadConfigFromLocalStorage() + await channel.messageAndWaitForResponse('SW', { target: 'SW', action: 'RELOAD_CONFIG' }) + await postFromIframeToParentSw() + setResetKey((prev) => prev + 1) // needed to ensure the config is re-rendered + }).catch(err => { + log.error('failed to probe for local gateway', err) + }) /** * On initial load, we want to send the config to the parent window, so that the reload page can auto-reload if enabled, and the subdomain registered service worker gets the latest config without user interaction. */ @@ -122,8 +172,9 @@ function ConfigPage (): React.JSX.Element | null { return (
- - + + + diff --git a/src/sw.ts b/src/sw.ts index 019c3e66..28ad655b 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -250,8 +250,10 @@ async function getVerifiedFetch (): Promise { const verifiedFetch = await createVerifiedFetch({ gateways: config.gateways, - routers: config.routers, - dnsResolvers + routers: config.delegatedRouting ? config.routers : [], + dnsResolvers, + allowInsecure: true, + allowLocal: true }, { contentTypeParser }) diff --git a/test-e2e/layout.test.ts b/test-e2e/layout.test.ts index c7efd7a6..dc1ff8fb 100644 --- a/test-e2e/layout.test.ts +++ b/test-e2e/layout.test.ts @@ -31,7 +31,7 @@ test.describe('smoketests', () => { const inputLocator = getConfigPageInput(page) // see https://playwright.dev/docs/locators#strictness await inputLocator.first().waitFor() - expect(await inputLocator.count()).toEqual(5) + expect(await inputLocator.count()).toEqual(6) const submitButton = getConfigPageSaveButton(page) await expect(submitButton).toBeVisible() })