@@ -0,0 +1,286 @@
"use strict";

const IpfsHttpClient = require("ipfs-http-client");
const ipns = require("ipns");
const IPFS = require("ipfs");
const pRetry = require("p-retry");
const base64url = require("base64url");
const last = require("it-last");
const cryptoKeys = require("human-crypto-keys"); // { getKeyPairFromSeed }

const { sleep, Logger, onEnterPress, catchAndLog } = require("./util");
const { Date, Element } = require("ipfs-utils/src/globalthis");

async function main() {
const apiUrlInput = document.getElementById("api-url");
const nodeConnectBtn = document.getElementById("node-connect");

const peerAddrInput = document.getElementById("peer-addr");
const wsConnectBtn = document.getElementById("peer-connect");

const ipnsInput = document.getElementById("topic");
const publishBtn = document.getElementById("publish");

const namespace = "/record/";
const retryOptions = {
retries: 5,
};

let log = Logger(document.getElementById("console"));
let sLog = Logger(document.getElementById("server-console"));
let keyName = "self";

let ipfsAPI; // remote server IPFS node
let ipfsBrowser; // local browser IPFS node
let peerId;

// init local browser node right away
log(`Browser IPFS getting ready...`);
ipfsBrowser = await IPFS.create({
pass: "01234567890123456789",
EXPERIMENTAL: { ipnsPubsub: true },
});
const { id } = await ipfsBrowser.id();
log(`Browser IPFS ready! Node id: ${id}`);
document.getElementById("api-url").disabled = false;
document.getElementById("node-connect").disabled = false;

async function nodeConnect(url) {
log(`Connecting to ${url}`);
ipfsAPI = IpfsHttpClient(url);
const { id, agentVersion } = await ipfsAPI.id();
peerId = id;
log(`<span class="green">Success!</span>`);
log(`Version ${agentVersion}`);
log(`Peer ID ${id}`);
document.getElementById("peer-addr").disabled = false;
document.getElementById("peer-connect").disabled = false;
}

async function wsConnect(addr) {
if (!addr) throw new Error("Missing peer multiaddr");
if (!ipfsBrowser)
throw new Error("Wait for the local IPFS node to start first");
log(`Connecting to peer ${addr}`);
await ipfsBrowser.swarm.connect(addr);
log(`<span class="green">Success!</span>`);
log("Listing swarm peers...");
await sleep();
const peers = await ipfsBrowser.swarm.peers();
peers.forEach((peer) => {
//console.log(`peer: ${JSON.stringify(peer, null, 2)}`);
const fullAddr = `${peer.addr}/ipfs/${peer.peer}`;
log(
`<span class="${
addr.endsWith(peer.peer) ? "teal" : ""
}">${fullAddr}</span>`
);
});
log(`(${peers.length} peers total)`);
document.getElementById("topic").disabled = false;
document.getElementById("publish").disabled = false;
}

// Wait until a peer subscribes a topic
const waitForPeerToSubscribe = async (daemon, topic) => {
await pRetry(async () => {
const res = await daemon.pubsub.ls();

if (!res || !res.length || !res.includes(topic)) {
throw new Error("Could not find subscription");
}

return res[0];
}, retryOptions);
};

// wait until a peer know about other peer to subscribe a topic
const waitForNotificationOfSubscription = (daemon, topic, peerId) =>
pRetry(async () => {
const res = await daemon.pubsub.peers(topic);

if (!res || !res.length || !res.includes(peerId)) {
throw new Error("Could not find peer subscribing");
}
}, retryOptions);

async function subs(node, topic, tLog) {
tLog(`Subscribing to ${topic}`);
await node.pubsub.subscribe(
topic,
(msg) => {
const from = msg.from;
const seqno = msg.seqno.toString("hex");

tLog(
`${new Date(
Date.now()
).toLocaleTimeString()}\n Message ${seqno} from ${from}`
);

let regex = "/record/";
if (topic.match(regex) ? topic.match(regex).length > 0 : false) {
tLog(
"\n#" +
ipns.unmarshal(msg.data).sequence.toString() +
") Topic: " +
msg.topicIDs[0].toString()
);
tLog("Value:\n" + ipns.unmarshal(msg.data).value.toString());
} else {
try {
tLog(JSON.stringify(msg.data.toString(), null, 2));
} catch (_) {
tLog(msg.data.toString("hex"));
}
}
},
{
onError: (err, fatal) => {
if (fatal) {
console.error(err);
tLog(`<span class="red">${err.message}</span>`);
tLog(`Resubscribing in 5s to ${topic}...`);
setTimeout(
catchAndLog(() => subs(node, topic, tLog), tLog),
5000
);
} else {
console.warn(err);
}
},
}
);
}

async function createKey(keyName) {
return new Promise(async (resolve, reject) => {
try {
// quick and dirty key gen, don't do this in real life
const key = await IPFS.multihashing.digest(
Buffer.from(keyName + Math.random().toString(36).substring(2)),
"sha2-256"
);
const keyPair = await cryptoKeys.getKeyPairFromSeed(key, "rsa");

// put it on the browser IPNS keychain and name it
await ipfsBrowser.key.import(keyName, keyPair.privateKey);
// now this key can be used to publish to this ipns publicKey
resolve(true);
} catch (err) {
console.log(`Error creating Key ${keyName}: \n ${err}`);
reject(false);
}
});
}

async function publish(content) {
if (!content) throw new Error("Missing ipns content to publish");
if (!ipfsAPI) throw new Error("Connect to a go-server node first");
if (!ipfsAPI.name.pubsub.state() || !ipfsBrowser.name.pubsub.state())
throw new Error(
"IPNS Pubsub must be enabled on bother peers, use --enable-namesys-pubsub"
);

log(`Publish to IPNS`); // subscribes the server to our IPNS topic

let browserNode = await ipfsBrowser.id();
let serverNode = await ipfsAPI.id();

// get which key this will be publish under, self or an imported custom key
keyName = document.querySelector('input[name="keyName"]:checked').value;
let keys = { name: "self", id: browserNode.id }; // default init

if (keyName != "self") {
if (!(await ipfsBrowser.key.list()).find((k) => k.name == keyName))
// skip if custom key exists already
await createKey(keyName);
let r = await ipfsBrowser.key.list();
keys = r.find((k) => k.name == keyName);
log(JSON.stringify(keys));
}

log(`Initial Resolve ${keys.id}`); // subscribes the server to our IPNS topic
last(ipfsAPI.name.resolve(keys.id, { stream: false })); // save the pubsub topic to the server to make them listen

await sleep(1000); // give it a moment to save it

// set up the topic from ipns key
let b58 = await IPFS.multihash.fromB58String(keys.id);
const ipnsKeys = ipns.getIdKeys(b58);
const topic = `${namespace}${base64url.encode(
ipnsKeys.routingKey.toBuffer()
)}`;

// subscribe and log on both nodes
await subs(ipfsBrowser, topic, log); // browserLog
await subs(ipfsAPI, topic, sLog); // serverLog

// confirm they are subscribed
await waitForPeerToSubscribe(ipfsAPI, topic); // confirm topic is on THEIR list // API
await waitForNotificationOfSubscription(ipfsBrowser, topic, serverNode.id); // confirm they are on OUR list

await sleep(2500); // give it a moment to save it

let remList = await ipfsAPI.pubsub.ls(); // API
if (!remList.includes(topic))
sLog(`<span class="red">[Fail] !Pubsub.ls ${topic}</span>`);
else sLog(`[Pass] Pubsub.ls`);

let remListSubs = await ipfsAPI.name.pubsub.subs(); // API
if (!remListSubs.includes(`/ipns/${keys.id}`))
sLog(`<span class="red">[Fail] !Name.Pubsub.subs ${keys.id}</span>`);
else sLog(`[Pass] Name.Pubsub.subs`);

// publish will send a pubsub msg to the server to update their ipns record
log(`Publishing ${content} to ${keys.name} /ipns/${keys.id}`);
const results = await ipfsBrowser.name.publish(content, {
resolve: false,
key: keyName,
});
log(`Published ${results.name} to ${results.value}`); //

log(`Wait 5 seconds, then resolve...`);
await sleep(5000);

log(`Try resolve ${keys.id} on server through API`);

let name = await last(
ipfsAPI.name.resolve(keys.id, {
stream: false,
})
);
log(`Resolved: ${name}`);
if (name == content) {
log(`<span class="green">IPNS Publish Success!</span>`);
log(
`<span class="green">Look at that! /ipns/${keys.id} resolves to ${content}</span>`
);
} else {
log(
`<span class="red">Error, resolve did not match ${name} !== ${content}</span>`
);
}
}

const onNodeConnectClick = catchAndLog(
() => nodeConnect(apiUrlInput.value),
log
);

apiUrlInput.addEventListener("keydown", onEnterPress(onNodeConnectClick));
nodeConnectBtn.addEventListener("click", onNodeConnectClick);

const onwsConnectClick = catchAndLog(
() => wsConnect(peerAddrInput.value),
log
);
peerAddrInput.addEventListener("keydown", onEnterPress(onwsConnectClick));
wsConnectBtn.addEventListener("click", onwsConnectClick);

const onPublishClick = catchAndLog(() => publish(ipnsInput.value), log);
ipnsInput.addEventListener("keydown", onEnterPress(onPublishClick));
publishBtn.addEventListener("click", onPublishClick);
}

main();
@@ -0,0 +1,44 @@
{
"name": "browser-ipns-publish",
"version": "1.0.0",
"description": "An example demonstrating publishing to IPNS in the browser",
"private": true,
"main": "index.js",
"scripts": {
"build": "parcel build index.html --public-url ./",
"start": "parcel index.html",
"test": "test-ipfs-example"
},
"author": "Doug Anderson",
"license": "MIT",
"dependencies": {
"base64url": "^3.0.1",
"human-crypto-keys": "^0.1.4",
"ipfs": "file:../../packages/ipfs/",
"ipfs-http-client": "file:../../packages/ipfs-http-client/",
"ipfs-utils": "^2.3.1",
"ipns": "^0.7.3",
"it-last": "^1.0.2",
"p-retry": "^4.2.0"
},
"browserslist": [
"last 2 versions and not dead and > 2%"
],
"devDependencies": {
"delay": "^4.3.0",
"execa": "^4.0.0",
"ipfsd-ctl": "^5.0.0",
"go-ipfs": "^0.6.0",
"parcel-bundler": "^1.12.4",
"path": "^0.12.7",
"test-ipfs-example": "file:../test-ipfs-example/"
},
"repository": {
"type": "git",
"url": "https://github.com/ipfs/js-ipfs/examples"
},
"keywords": [
"IPNS",
"Publish"
]
}
@@ -0,0 +1,136 @@
"use strict";

const path = require("path");
const execa = require("execa");
const { createFactory } = require("ipfsd-ctl");
const df = createFactory({
ipfsHttpModule: require("ipfs-http-client"),
ipfsBin: require("go-ipfs").path(),
args: ["--enable-pubsub-experiment"],
disposable: true,
});
const { startServer } = require("test-ipfs-example/utils");
const pkg = require("./package.json");

async function testUI(url, apiAddr, peerAddr, topic) {
const proc = execa(
require.resolve("test-ipfs-example/node_modules/.bin/nightwatch"),
[
"--config",
require.resolve("test-ipfs-example/nightwatch.conf.js"),
path.join(__dirname, "test.js"),
],
{
cwd: path.resolve(__dirname, "../"),
env: {
...process.env,
CI: true,
IPFS_EXAMPLE_TEST_URL: url,
IPFS_API_ADDRESS: apiAddr,
IPFS_PEER_ADDRESS: peerAddr,
IPFS_TOPIC: topic,
},
all: true,
}
);
proc.all.on("data", (data) => {
process.stdout.write(data);
});

await proc;
}

async function runTest() {
const app = await startServer(__dirname);
const go = await df.spawn({
type: "go",
test: true,
ipfsOptions: {
config: {
Addresses: {
API: "/ip4/127.0.0.1/tcp/0",
},
API: {
HTTPHeaders: {
"Access-Control-Allow-Origin": [app.url],
},
},
},
},
});

const go2 = await df.spawn({
type: "go",
test: true,
});

await go.api.swarm.connect(go2.api.peerId.addresses[0]);

const topic = `/ipfs/QmWCnkCXYYPP7NgH6ZHQiQxw7LJAMjnAySdoz9i1oxD5XJ`;

try {
await testUI(
app.url,
go.apiAddr,
go.api.peerId.addresses[0].toString(),
topic
);
} finally {
await go.stop();
await go2.stop();
await app.stop();
}
}

module.exports = runTest;

module.exports[pkg.name] = function (browser) {
const apiSelector = "#api-url:enabled";

// connect to the API
browser
.url(process.env.IPFS_EXAMPLE_TEST_URL)
.waitForElementVisible(apiSelector)
.clearValue(apiSelector)
.setValue(apiSelector, process.env.IPFS_API_ADDRESS)
.pause(1000)
.perform(() => {
console.log(
"process.env.IPFS_API_ADDRESS: ",
process.env.IPFS_API_ADDRESS
);
})
.click("#node-connect");

browser.expect
.element("#console")
.text.to.contain(`Connecting to ${process.env.IPFS_API_ADDRESS}\nSuccess!`);

// connect via websocket
const peerAddrSelector = "#peer-addr:enabled";
browser
.waitForElementVisible(peerAddrSelector)
.clearValue(peerAddrSelector)
.setValue(peerAddrSelector, process.env.IPFS_PEER_ADDRESS)
.pause(1000)
.click("#peer-connect");

browser.expect
.element("#console")
.text.to.contain(
`Connecting to peer ${process.env.IPFS_PEER_ADDRESS}\nSuccess!`
);

// publish to IPNS
const publishSelector = "#topic:enabled";
browser
.waitForElementVisible(publishSelector)
.clearValue(publishSelector)
.setValue(publishSelector, process.env.IPFS_TOPIC)
.pause(1000)
.click("#publish");

browser.expect.element("#console").text.to.contain(`IPNS Publish Success!`);

browser.end();
};
@@ -0,0 +1,31 @@
exports.sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms))

exports.Logger = outEl => {
outEl.innerHTML = ''
return message => {
const container = document.createElement('div')
container.innerHTML = message
outEl.appendChild(container)
outEl.scrollTop = outEl.scrollHeight
}
}

exports.onEnterPress = fn => {
return e => {
if (event.which == 13 || event.keyCode == 13) {
e.preventDefault()
fn()
}
}
}

exports.catchAndLog = (fn, log) => {
return async (...args) => {
try {
await fn(...args)
} catch (err) {
console.error(err)
log(`<span class="red">${err.message}</span>`)
}
}
}
@@ -2,6 +2,7 @@

const { ephemeralPort } = require('./utils')
const path = require('path')
const chrome = require('chromedriver') // added to work on windows

const WEBRIVER_PORT = ephemeralPort()

@@ -11,7 +12,7 @@ module.exports = {

webdriver: {
start_process: true,
server_path: require.resolve(path.resolve(__dirname, 'node_modules/.bin/chromedriver')),
server_path: chrome.path, //require.resolve(path.resolve(__dirname, 'node_modules/.bin/chromedriver')), // this wasn't working on windows
port: WEBRIVER_PORT,
cli_args: [
`--port=${WEBRIVER_PORT}`
@@ -10,7 +10,7 @@
"chromedriver": "^83.0.0",
"execa": "^4.0.0",
"fs-extra": "^8.1.0",
"http-server": "^0.11.1",
"http-server": "^0.12.3",
"nightwatch": "^1.2.4",
"which": "^2.0.1"
}
@@ -25,7 +25,7 @@ async function testExample (dir) {
//await installDeps(dir)
await build(dir)

if (dir.includes('examples/browser-')) {
if (dir.includes('examples/browser-') || dir.includes('examples\\browser-') ) {
await runBrowserTest(dir)
} else {
await runNodeTest(dir)
@@ -1,159 +1,182 @@
'use strict'
"use strict";

const fs = require('fs-extra')
const path = require('path')
const execa = require('execa')
const which = require('which')
const fs = require("fs-extra");
const path = require("path");
const execa = require("execa");
const which = require("which");

async function startServer (dir) {
async function serveFrom (appDir) {
async function startServer(dir) {
async function serveFrom(appDir) {
return new Promise((resolve, reject) => {
let output = ''

const proc = execa.command(`${path.resolve(__dirname, 'node_modules/.bin/http-server')} ${appDir} -a 127.0.0.1`, {
cwd: __dirname,
all: true
})
proc.all.on('data', (data) => {
process.stdout.write(data)

const line = data.toString('utf8')
output += line

if (output.includes('Hit CTRL-C to stop the server')) {
// find the port
const port = output.match(/http:\/\/127.0.0.1:(\d+)/)[1]
let output = "";

const proc = execa.command(
`${path.resolve(
__dirname,
"node_modules/.bin/http-server"
)} ${appDir} -a 127.0.0.1`,
{
cwd: __dirname,
all: true,
}
);
proc.all.on("data", (data) => {
process.stdout.write(data);

const line = data.toString("utf8");
output += line;

let port;
if (output.includes("Hit CTRL-C to stop the server")) {
// windows hack, windows inserts a bunch of unicode around the port
if (output.includes(`\u001b[32m`)) {
port = output.split(`\u001b[32m`)[1].split(`\u001b[39m`)[0];
} else {
// find the port
const regex = /http:\/\/127.0.0.1:(\d+)/; //|(?<=\\u001b\[32m)(.\d+?)(?=\\u001b\[39m)/; // positive lookbehind regex didn't work
port = output.match(regex)[output.match(regex).length - 1];
}

if (!port) {
throw new Error(`Could not find port in ${output}`)
throw new Error(`Could not find port in ${output}`);
}

resolve({
stop: () => {
console.info('Stopping server')
proc.kill('SIGINT', {
forceKillAfterTimeout: 2000
})
console.info("Stopping server");
proc.kill("SIGINT", {
forceKillAfterTimeout: 2000,
});
},
url: `http://127.0.0.1:${port}`
})
url: `http://127.0.0.1:${port}`,
});
}
})
});

proc.then(() => {}, (err) => reject(err))
})
proc.then(
() => {},
(err) => reject(err)
);
});
}

// start something..
const serverPaths = [
path.join(dir, 'build'),
path.join(dir, 'dist'),
path.join(dir, 'public')
]
path.join(dir, "build"),
path.join(dir, "dist"),
path.join(dir, "public"),
];

for (const p of serverPaths) {
if (fs.existsSync(p)) {
return serveFrom(p)
return serveFrom(p);
}
}

// running a bare index.html file
const files = [
path.join(dir, 'index.html')
]
const files = [path.join(dir, "index.html")];

for (const f of files) {
if (fs.existsSync(f)) {
console.info('Found bare file', f)
console.info("Found bare file", f);

return Promise.resolve({
url: `file://${f}`,
stop: () => {}
})
stop: () => {},
});
}
}

throw new Error('Browser examples must contain a `public`, `dist` or `build` folder or an `index.html` file')
throw new Error(
"Browser examples must contain a `public`, `dist` or `build` folder or an `index.html` file"
);
}

function ephemeralPort (min = 49152, max = 65535) {
return Math.floor(Math.random() * (max - min + 1) + min)
function ephemeralPort(min = 49152, max = 65535) {
return Math.floor(Math.random() * (max - min + 1) + min);
}

async function isExecutable (command) {
async function isExecutable(command) {
try {
await fs.access(command, fs.constants.X_OK)
await fs.access(command, fs.constants.X_OK);

return true
return true;
} catch (err) {
if (err.code === 'ENOENT') {
return isExecutable(await which(command))
if (err.code === "ENOENT") {
return isExecutable(await which(command));
}

if (err.code === 'EACCES') {
return false
if (err.code === "EACCES") {
return false;
}

throw err
throw err;
}
}

async function waitForOutput (expectedOutput, command, args = [], opts = {}) {
if (!await isExecutable(command)) {
args.unshift(command)
command = 'node'
async function waitForOutput(expectedOutput, command, args = [], opts = {}) {
if (!(await isExecutable(command))) {
args.unshift(command);
command = "node";
}

const proc = execa(command, args, { ...opts, all: true })
let output = ''
const time = 120000
const proc = execa(command, args, { ...opts, all: true });
let output = "";
const time = 120000;

let foundExpectedOutput = false
let cancelTimeout
let foundExpectedOutput = false;
let cancelTimeout;
const timeoutPromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Did not see "${expectedOutput}" in output from "${[command].concat(args).join(' ')}" after ${time / 1000}s`))
reject(
new Error(
`Did not see "${expectedOutput}" in output from "${[command]
.concat(args)
.join(" ")}" after ${time / 1000}s`
)
);

setTimeout(() => {
proc.kill()
}, 100)
}, time)
proc.kill();
}, 100);
}, time);

cancelTimeout = () => {
clearTimeout(timeout)
resolve()
}
})
clearTimeout(timeout);
resolve();
};
});

proc.all.on('data', (data) => {
process.stdout.write(data)
output += data.toString('utf8')
proc.all.on("data", (data) => {
process.stdout.write(data);
output += data.toString("utf8");

if (output.includes(expectedOutput)) {
foundExpectedOutput = true
proc.kill()
cancelTimeout()
foundExpectedOutput = true;
proc.kill();
cancelTimeout();
}
})
});

try {
await Promise.race([
proc,
timeoutPromise
])
await Promise.race([proc, timeoutPromise]);
} catch (err) {
if (!err.killed) {
throw err
throw err;
}
}

if (!foundExpectedOutput) {
throw new Error(`Did not see "${expectedOutput}" in output from "${[command].concat(args).join(' ')}"`)
throw new Error(
`Did not see "${expectedOutput}" in output from "${[command]
.concat(args)
.join(" ")}"`
);
}
}

module.exports = {
startServer,
ephemeralPort,
waitForOutput
}
waitForOutput,
};
@@ -1,15 +1,18 @@
{
"lerna": "3.22.0",
"packages": [
"packages/*"
"packages/*",
"examples/*"
],
"version": "independent",
"command": {
"bootstrap": {
"hoist": true,
"nohoist": [
"libp2p-delegated-content-routing",
"libp2p-delegated-peer-routing"
"libp2p-delegated-peer-routing",
"ipfs-css",
"tachyons"
]
},
"run": {