/// import fetch from "node-fetch" import os from "os" import path from "path" import itAll from "it-all" import itMap from "it-map" import inquirer from "inquirer" import { CID } from "multiformats" import { EventEmitter } from "events" import * as uint8arrays from "uint8arrays" import * as webnative from "webnative-0.30" import * as ed25519 from "@noble/ed25519" import { CLIContext, createContext } from "./context.js" import { createFissionConnectedIPFS, runFissionIpfsCommand } from "../fission/ipfs.js" import * as fs_1_0_0 from "../versions/fs-1.0.0.js" import * as fs_2_0_0 from "../versions/fs-2.0.0.js" export async function run() { EventEmitter.defaultMaxListeners = 0 const context = createContext(path.join(os.homedir(), ".config/fission"), webnative.setup.endpoints({})) console.log(`Looking up data root for ${context.fissionConfig.username}`) const dataRoot = await figureOutDataRoot(context) console.log(`Data root is ${dataRoot}`) console.log("Loading IPFS...") const { ipfs, controller } = await createFissionConnectedIPFS(context) // will log success process.on("SIGTERM", () => controller.abort()) try { console.log(`Looking up your filesystem version (https://${context.fissionConfig.username}.files.fission.name/version)`) const version = uint8arrays.toString(uint8arrays.concat(await itAll(ipfs.cat(`${dataRoot}/version`)))) console.log(`Your filesystem currently is at version ${version}`) if (version === "2.0.0") { throw new Error(`This migration tool is made for migrations from version 1.0.0 to 2.0.0. This account has already been migrated.`) } if (version !== "1.0.0") { throw new Error(`This migration tool is made for migrations from version 1.0.0 to 2.0.0. You might need a newer version of this migration tool.`) } const readKey = context.wnfsReadKey const migratedCID = await fs_2_0_0.filesystemFromEntries( itMap(fs_1_0_0.traverseFileSystem(ipfs, dataRoot, readKey), async entry => { console.log(`Processing ${webnative.path.toPosix(webnative.path.file(...entry.path))}`) return entry }), ipfs, readKey ) console.log(`Finished migration: ${migratedCID}`) const answers = await inquirer.prompt([{ name: "confirmed", type: "confirm", message: "Are you sure you want to overwrite your filesystem with a migrated version?" }]) if (!answers.confirmed) { throw new Error(`User cancelled.`) } console.log(`Creating authorization UCAN.`) const ucan = await figureOutUcan(context, controller.signal) console.log(`Created authorization UCAN. Updating data root...`) while (!controller.signal.aborted) { try { await setDataRoot(migratedCID, `Bearer ${ucan}`, context, controller.signal) } catch (e) { console.error("Error while setting data root:") console.error(e) console.error("Retrying...") } } console.log(`Migration done!`) } catch (e) { console.error(e) process.exitCode = 1 } finally { console.log(`Shutting down IPFS...`) await ipfs.stop() controller.abort() // 5 sec timeout await new Promise(resolve => setTimeout(resolve, 5000)) process.exit() } } async function figureOutDataRoot(context: CLIContext): Promise { const dataRoot = await getUsernameDataRoot(context.fissionConfig.username, context) if (dataRoot == null) { throw new Error(`Your account either doesn't exist or doesn't have a filesystem attached to it. Most likely because it was created from the fission CLI and not from the browser. Please try linking a browser-based account using the fission CLI.`) } return dataRoot } async function figureOutUcan(context: CLIContext, signal: AbortSignal): Promise { let proof if (context.fissionConfigRootProof != null) { const tokenPath = `/ipfs/${context.fissionConfigRootProof.toString()}/bearer.jwt` const token = await runFissionIpfsCommand(["cat", tokenPath], context, timeout(10000, signal)) const resolved = JSON.parse(token) if (typeof resolved !== "string") { throw new Error(`Couldn't parse UCAN at ${tokenPath}: ${resolved}`) } const bearerPrefix = "Bearer " if (!resolved.startsWith(bearerPrefix)) { throw new Error(`Couldn't parse UCAN at ${tokenPath}: ${resolved}`) } proof = resolved.substring(bearerPrefix.length) } const pubKey = await ed25519.getPublicKey(context.writeKey) const ourDid = webnative.did.publicKeyToDid(uint8arrays.toString(pubKey, "base64pad"), webnative.did.KeyType.Edwards) const ucanParts = await webnative.ucan.build({ addSignature: false, audience: context.fissionConfig.server_did, issuer: ourDid, potency: "APPEND", proof, resource: "*", }) // monkey-patch the algorithm to match what we're doing ucanParts.header.alg = "EdDSA" const encoded = { header: webnative.ucan.encodeHeader(ucanParts.header), payload: webnative.ucan.encodePayload(ucanParts.payload), } const headerAndPayload = `${encoded.header}.${encoded.payload}` const signature = uint8arrays.toString(await ed25519.sign(uint8arrays.fromString(headerAndPayload), context.writeKey), "base64urlpad") return `${headerAndPayload}.${signature}` } async function getUsernameDataRoot(username: string, context: CLIContext): Promise { const apiEndpoint = `${context.endpoints.api}/${context.endpoints.apiVersion}/api` const resp = await fetch(`${apiEndpoint}/user/data/${username}`) if (!resp.ok) { return null } const respStr = await resp.json() if (typeof respStr !== "string") { throw new Error(`Unexpected response for data root lookup for ${username}: ${respStr}`) } try { return CID.parse(respStr) } catch { return null } } // The JWT identifiers the user async function setDataRoot(dataRoot: CID, jwt: string, context: CLIContext, signal?: AbortSignal): Promise { const apiEndpoint = `${context.endpoints.api}/${context.endpoints.apiVersion}/api` const resp = await fetch(`${apiEndpoint}/user/data/${dataRoot.toString()}`, { method: "PUT", headers: { "authorization": jwt }, signal, }) if (!resp.ok) { throw new Error(`Failed to update data root. HTTP Code ${resp.status}. Message: ${await resp.text()}`) } } function timeout(ms: number, signal?: AbortSignal): AbortSignal { const controller = new AbortController() const id = setTimeout(() => { controller.abort() signal?.removeEventListener("abort", onOuterAbort) }, ms) // Yes, this is weird. // eslint-disable-next-line no-inner-declarations function onOuterAbort() { clearTimeout(id) controller.abort() } signal?.addEventListener("abort", onOuterAbort, { once: true }) return controller.signal }