fission-suite/webnative

View on GitHub
src/components/reference/implementation/fission/data-root.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { CID } from "multiformats/cid"

import * as DID from "../../../../did/index.js"
import * as DNS from "../../dns-over-https.js"
import * as Fission from "../../../../common/fission.js"
import * as TypeChecks from "../../../../common/type-checks.js"
import * as Ucan from "../../../../ucan/index.js"

import { decodeCID } from "../../../../common/cid.js"
import { Dependencies } from "../base.js"


/**
 * Get the CID of a user's data root.
 * First check Fission server, then check DNS
 *
 * @param username The username of the user that we want to get the data root of.
 */
export async function lookup(
  endpoints: Fission.Endpoints,
  dependencies: Dependencies,
  username: string
): Promise<CID | null> {
  const maybeRoot = await lookupOnFisson(endpoints, dependencies, username)
  if (!maybeRoot) return null
  if (maybeRoot !== null) return maybeRoot

  try {
    const cid = await DNS.lookupDnsLink(username + ".files." + endpoints.userDomain)
    return !cid ? null : decodeCID(cid)
  } catch (err) {
    console.error(err)
    throw new Error("Could not locate user root in DNS")
  }
}

/**
 * Get the CID of a user's data root from the Fission server.
 *
 * @param username The username of the user that we want to get the data root of.
 */
export async function lookupOnFisson(
  endpoints: Fission.Endpoints,
  dependencies: Dependencies,
  username: string
): Promise<CID | null> {
  try {
    const resp = await fetch(
      Fission.apiUrl(endpoints, `user/data/${username}`),
      { cache: "reload" } // don't use cache
    )
    const cid = await resp.json()
    return decodeCID(cid)

  } catch (err) {
    dependencies.manners.log(
      "Could not locate user root on Fission server: ",
      TypeChecks.hasProp(err, "toString") ? (err as any).toString() : err
    )
    return null

  }
}

/**
 * Update a user's data root.
 *
 * @param cid The CID of the data root.
 * @param proof The proof to use in the UCAN sent to the API.
 */
export async function update(
  endpoints: Fission.Endpoints,
  dependencies: Dependencies,
  cidInstance: CID,
  proof: Ucan.Ucan
): Promise<{ success: boolean }> {
  const cid = cidInstance.toString()

  // Debug
  dependencies.manners.log("🌊 Updating your DNSLink:", cid)

  // Make API call
  return await fetchWithRetry(Fission.apiUrl(endpoints, `user/data/${cid}`), {
    headers: async () => {
      const jwt = Ucan.encode(await Ucan.build({
        dependencies: dependencies,

        audience: await Fission.did(endpoints),
        issuer: await DID.ucan(dependencies.crypto),
        potency: "APPEND",
        proof: Ucan.encode(proof),

        // TODO: Waiting on API change.
        //       Should be `username.fission.name/*`
        resource: proof.payload.rsc
      }))

      return { "authorization": `Bearer ${jwt}` }
    },
    retries: 100,
    retryDelay: 5000,
    retryOn: [ 502, 503, 504 ],

  }, {
    method: "PUT"

  }).then((response: Response) => {
    if (response.status < 300) dependencies.manners.log("🪴 DNSLink updated:", cid)
    else dependencies.manners.log("🔥 Failed to update DNSLink for:", cid)
    return { success: response.status < 300 }

  }).catch(err => {
    dependencies.manners.log("🔥 Failed to update DNSLink for:", cid)
    console.error(err)
    return { success: false }

  })
}



// ㊙️


type RetryOptions = {
  headers: () => Promise<{ [ _: string ]: string }>
  retries: number
  retryDelay: number
  retryOn: Array<number>
}


async function fetchWithRetry(
  url: string,
  retryOptions: RetryOptions,
  fetchOptions: RequestInit,
  retry = 0
): Promise<Response> {
  const headers = await retryOptions.headers()
  const response = await fetch(url, {
    ...fetchOptions,
    headers: { ...fetchOptions.headers, ...headers }
  })

  if (retryOptions.retryOn.includes(response.status)) {
    if (retry < retryOptions.retries) {
      return await new Promise((resolve, reject) => setTimeout(
        () => fetchWithRetry(url, retryOptions, fetchOptions, retry + 1).then(resolve, reject),
        retryOptions.retryDelay
      ))
    } else {
      throw new Error("Too many retries for fetch")
    }
  }

  return response
}