Sfoglia il codice sorgente

media: commands family - channels and videos

Leszek Wiesner 4 anni fa
parent
commit
8a19401ed8

+ 13 - 0
cli/package.json

@@ -9,6 +9,7 @@
   "bugs": "https://github.com/Joystream/joystream/issues",
   "dependencies": {
     "@apidevtools/json-schema-ref-parser": "^9.0.6",
+    "@ffmpeg-installer/ffmpeg": "^1.0.20",
     "@joystream/types": "^0.14.0",
     "@oclif/command": "^1.5.19",
     "@oclif/config": "^1.14.0",
@@ -17,12 +18,21 @@
     "@oclif/plugin-not-found": "^1.2.4",
     "@oclif/plugin-warn-if-update-available": "^1.7.0",
     "@polkadot/api": "1.26.1",
+    "@types/fluent-ffmpeg": "^2.1.16",
     "@types/inquirer": "^6.5.0",
     "@types/proper-lockfile": "^4.1.1",
     "@types/slug": "^0.9.1",
     "ajv": "^6.11.0",
     "cli-ux": "^5.4.5",
+    "fluent-ffmpeg": "^2.1.2",
     "inquirer": "^7.1.0",
+    "ipfs-http-client": "^47.0.1",
+    "ipfs-only-hash": "^1.0.2",
+    "it-all": "^1.0.4",
+    "it-drain": "^1.0.3",
+    "it-first": "^1.0.4",
+    "it-last": "^1.0.4",
+    "it-to-buffer": "^1.0.4",
     "moment": "^2.24.0",
     "proper-lockfile": "^4.1.1",
     "slug": "^2.1.1",
@@ -90,6 +100,9 @@
       },
       "content-directory": {
         "description": "Interactions with content directory module - managing classes, schemas, entities and permissions"
+      },
+      "media": {
+        "description": "Higher-level content directory interactions, ie. publishing and curating content"
       }
     }
   },

+ 3 - 0
cli/src/@types/@ffmpeg-installer/ffmpeg/index.d.ts

@@ -0,0 +1,3 @@
+declare module '@ffmpeg-installer/ffmpeg' {
+  export const path: string
+}

+ 1 - 0
cli/src/@types/ipfs-http-client/index.d.ts

@@ -0,0 +1 @@
+declare module 'ipfs-http-client'

+ 1 - 0
cli/src/@types/ipfs-only-hash/index.d.ts

@@ -0,0 +1 @@
+declare module 'ipfs-only-hash'

+ 21 - 0
cli/src/Api.ts

@@ -48,6 +48,9 @@ import { Stake, StakeId } from '@joystream/types/stake'
 
 import { InputValidationLengthConstraint } from '@joystream/types/common'
 import { Class, ClassId, CuratorGroup, CuratorGroupId, Entity, EntityId } from '@joystream/types/content-directory'
+import { ContentId, DataObject } from '@joystream/types/media'
+import { ServiceProviderRecord, Url } from '@joystream/types/discovery'
+import _ from 'lodash'
 
 export const DEFAULT_API_URI = 'ws://localhost:9944/'
 const DEFAULT_DECIMALS = new BN(12)
@@ -512,4 +515,22 @@ export default class Api {
     const exists = !!(await this._api.query.contentDirectory.curatorGroupById.size(id))
     return exists ? await this._api.query.contentDirectory.entityById<Entity>(id) : null
   }
+
+  async dataObjectByContentId(contentId: ContentId): Promise<DataObject | null> {
+    const dataObject = await this._api.query.dataDirectory.dataObjectByContentId<Option<DataObject>>(contentId)
+    return dataObject.unwrapOr(null)
+  }
+
+  async ipnsIdentity(storageProviderId: number): Promise<string | null> {
+    const accountInfo = await this._api.query.discovery.accountInfoByStorageProviderId<ServiceProviderRecord>(
+      storageProviderId
+    )
+    return accountInfo.isEmpty ? null : accountInfo.identity.toString()
+  }
+
+  async getRandomBootstrapEndpoint(): Promise<string | null> {
+    const endpoints = await this._api.query.discovery.bootstrapEndpoints<Vec<Url>>()
+    const randomEndpoint = _.sample(endpoints.toArray())
+    return randomEndpoint ? randomEndpoint.toString() : null
+  }
 }

+ 1 - 0
cli/src/ExitCodes.ts

@@ -11,5 +11,6 @@ enum ExitCodes {
   UnexpectedException = 500,
   FsOperationFailed = 501,
   ApiError = 502,
+  ExternalInfrastructureError = 503,
 }
 export = ExitCodes

+ 10 - 0
cli/src/base/AccountsCommandBase.ts

@@ -224,6 +224,16 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     }
   }
 
+  async getRequiredMemberId(): Promise<number> {
+    const account = await this.getRequiredSelectedAccount()
+    const memberIds = await this.getApi().getMemberIdsByControllerAccount(account.address)
+    if (!memberIds.length) {
+      this.error('Membership required to access this command!', { exit: ExitCodes.AccessDenied })
+    }
+
+    return memberIds[0].toNumber() // FIXME: Temporary solution (just using the first one)
+  }
+
   async init() {
     await super.init()
     try {

+ 64 - 2
cli/src/base/ContentDirectoryCommandBase.ts

@@ -2,11 +2,13 @@ import ExitCodes from '../ExitCodes'
 import AccountsCommandBase from './AccountsCommandBase'
 import { WorkingGroups, NamedKeyringPair } from '../Types'
 import { ReferenceProperty } from 'cd-schemas/types/extrinsics/AddClassSchema'
+import { FlattenRelations } from 'cd-schemas/types/utility'
 import { BOOL_PROMPT_OPTIONS } from '../helpers/prompting'
-import { Class, ClassId, CuratorGroup, CuratorGroupId, Entity } from '@joystream/types/content-directory'
+import { Class, ClassId, CuratorGroup, CuratorGroupId, Entity, EntityId } from '@joystream/types/content-directory'
 import { Worker } from '@joystream/types/working-group'
 import { CLIError } from '@oclif/errors'
 import { Codec } from '@polkadot/types/types'
+import _ from 'lodash'
 
 /**
  * Abstract base class for commands related to working groups
@@ -137,12 +139,63 @@ export default abstract class ContentDirectoryCommandBase extends AccountsComman
     const entity = await this.getApi().entityById(id)
 
     if (!entity) {
-      this.error('Invalid entity id!', { exit: ExitCodes.InvalidInput })
+      this.error(`Entity not found by id: ${id}`, { exit: ExitCodes.InvalidInput })
     }
 
     return entity
   }
 
+  async getAndParseKnownEntity<T>(id: string | number): Promise<FlattenRelations<T>> {
+    const entity = await this.getEntity(id)
+    return this.parseToKnownEntityJson<T>(entity)
+  }
+
+  async promptForEntityEntry(
+    message: string,
+    className: string,
+    propName?: string,
+    ownerMemberId?: number,
+    defaultId?: number
+  ): Promise<[EntityId, Entity]> {
+    const [classId, entityClass] = await this.classEntryByNameOrId(className)
+    const entityEntries = (await this.getApi().entitiesByClassId(classId.toNumber())).filter(([, entity]) => {
+      const controller = entity.entity_permissions.controller
+      return ownerMemberId !== undefined
+        ? controller.isOfType('Member') && controller.asType('Member').toNumber() === ownerMemberId
+        : true
+    })
+
+    if (!entityEntries.length) {
+      this.log(`${message}:`)
+      this.error(`No choices available! Exiting...`, { exit: ExitCodes.UnexpectedException })
+    }
+
+    const choosenEntityId = await this.simplePrompt({
+      message,
+      type: 'list',
+      choices: entityEntries.map(([id, entity]) => {
+        const parsedEntityPropertyValues = this.parseEntityPropertyValues(entity, entityClass)
+        return {
+          name: (propName && parsedEntityPropertyValues[propName]?.value.toString()) || `ID:${id.toString()}`,
+          value: id.toString(), // With numbers there are issues with "default"
+        }
+      }),
+      default: defaultId?.toString(),
+    })
+
+    return entityEntries.find(([id]) => choosenEntityId === id.toString())!
+  }
+
+  async promptForEntityId(
+    message: string,
+    className: string,
+    propName?: string,
+    ownerMemberId?: number,
+    defaultId?: number
+  ): Promise<number> {
+    return (await this.promptForEntityEntry(message, className, propName, ownerMemberId, defaultId))[0].toNumber()
+  }
+
   parseEntityPropertyValues(
     entity: Entity,
     entityClass: Class,
@@ -163,4 +216,13 @@ export default abstract class ContentDirectoryCommandBase extends AccountsComman
       return columns
     }, {} as Record<string, { value: Codec; type: string }>)
   }
+
+  async parseToKnownEntityJson<T>(entity: Entity, entityClass?: Class): Promise<FlattenRelations<T>> {
+    if (!entityClass) {
+      entityClass = (await this.classEntryByNameOrId(entity.class_id.toString()))[1]
+    }
+    return (_.mapValues(this.parseEntityPropertyValues(entity, entityClass), (v) =>
+      v.value.toJSON()
+    ) as unknown) as FlattenRelations<T>
+  }
 }

+ 1 - 1
cli/src/commands/content-directory/addClassSchema.ts

@@ -20,7 +20,7 @@ export default class AddClassSchemaCommand extends ContentDirectoryCommandBase {
 
     const { input, output } = this.parse(AddClassSchemaCommand).flags
 
-    let inputJson = getInputJson<AddClassSchema>(input)
+    let inputJson = await getInputJson<AddClassSchema>(input)
     if (!inputJson) {
       let selectedClass: Class | undefined
       const customPrompts: JsonSchemaCustomPrompts = [

+ 2 - 1
cli/src/commands/content-directory/class.ts

@@ -37,7 +37,8 @@ export default class ClassCommand extends ContentDirectoryCommandBase {
     displayHeader(`Properties`)
     if (aClass.properties.length) {
       displayTable(
-        aClass.properties.map((p) => ({
+        aClass.properties.map((p, i) => ({
+          'Index': i,
           'Name': p.name.toString(),
           'Type': JSON.stringify(p.property_type.toJSON()),
           'Required': p.required.toString(),

+ 2 - 2
cli/src/commands/content-directory/createClass.ts

@@ -18,7 +18,7 @@ export default class CreateClassCommand extends ContentDirectoryCommandBase {
 
     const { input, output } = this.parse(CreateClassCommand).flags
 
-    let inputJson = getInputJson<CreateClass>(input)
+    let inputJson = await getInputJson<CreateClass>(input, CreateClassSchema as JSONSchema)
     if (!inputJson) {
       const customPrompts: JsonSchemaCustomPrompts = [
         ['class_permissions.maintainers', () => this.promptForCuratorGroups('Select class maintainers')],
@@ -36,7 +36,7 @@ export default class CreateClassCommand extends ContentDirectoryCommandBase {
       await this.requestAccountDecoding(account)
       this.log('Sending the extrinsic...')
       const inputParser = new InputParser(this.getOriginalApi())
-      await this.sendAndFollowTx(account, inputParser.parseCreateClassExtrinsic(inputJson))
+      await this.sendAndFollowTx(account, inputParser.parseCreateClassExtrinsic(inputJson), true)
 
       saveOutputJson(output, `${inputJson.name}Class.json`, inputJson)
     }

+ 53 - 0
cli/src/commands/media/createChannel.ts

@@ -0,0 +1,53 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import ChannelEntitySchema from 'cd-schemas/schemas/entities/ChannelEntity.schema.json'
+import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+import { InputParser } from 'cd-schemas'
+import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
+import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
+import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
+
+export default class CreateChannelCommand extends ContentDirectoryCommandBase {
+  static description = 'Create a new channel on Joystream (requires a membership).'
+  static flags = {
+    ...IOFlags,
+  }
+
+  async run() {
+    const account = await this.getRequiredSelectedAccount()
+    const memberId = await this.getRequiredMemberId()
+    const actor = { Member: memberId }
+
+    await this.requestAccountDecoding(account)
+
+    const channelJsonSchema = (ChannelEntitySchema as unknown) as JSONSchema
+
+    const { input, output } = this.parse(CreateChannelCommand).flags
+
+    let inputJson = await getInputJson<ChannelEntity>(input, channelJsonSchema)
+    if (!inputJson) {
+      const customPrompts: JsonSchemaCustomPrompts = [
+        ['language', () => this.promptForEntityId('Choose channel language', 'Language', 'name')],
+        ['curationStatus', async () => undefined],
+      ]
+
+      const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, undefined, customPrompts)
+
+      inputJson = await prompter.promptAll()
+    }
+
+    this.jsonPrettyPrint(JSON.stringify(inputJson))
+    const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+
+    if (confirmed) {
+      const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi(), [
+        {
+          className: 'Channel',
+          entries: [inputJson],
+        },
+      ])
+      const operations = await inputParser.getEntityBatchOperations()
+      await this.sendAndFollowNamedTx(account, 'contentDirectory', 'transaction', [actor, operations], true)
+      saveOutputJson(output, `${inputJson.title}Channel.json`, inputJson)
+    }
+  }
+}

+ 61 - 0
cli/src/commands/media/updateChannel.ts

@@ -0,0 +1,61 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import ChannelEntitySchema from 'cd-schemas/schemas/entities/ChannelEntity.schema.json'
+import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+import { InputParser } from 'cd-schemas'
+import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
+import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
+import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
+
+export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
+  static description = 'Update one of the owned channels on Joystream (requires a membership).'
+  static flags = {
+    ...IOFlags,
+  }
+  // TODO: ChannelId as arg?
+
+  async run() {
+    const account = await this.getRequiredSelectedAccount()
+    const memberId = await this.getRequiredMemberId()
+    const actor = { Member: memberId }
+
+    const [channelId, channel] = await this.promptForEntityEntry(
+      'Select a channel to update',
+      'Channel',
+      'title',
+      memberId
+    )
+    const currentValues = await this.parseToKnownEntityJson<ChannelEntity>(channel)
+    this.jsonPrettyPrint(JSON.stringify(currentValues))
+
+    const channelJsonSchema = (ChannelEntitySchema as unknown) as JSONSchema
+
+    const { input, output } = this.parse(UpdateChannelCommand).flags
+
+    let inputJson = await getInputJson<ChannelEntity>(input, channelJsonSchema)
+    if (!inputJson) {
+      const customPrompts: JsonSchemaCustomPrompts = [
+        [
+          'language',
+          () => this.promptForEntityId('Choose channel language', 'Language', 'name', currentValues.language),
+        ],
+        ['curationStatus', async () => undefined],
+      ]
+
+      const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, currentValues, customPrompts)
+
+      inputJson = await prompter.promptAll()
+    }
+
+    this.jsonPrettyPrint(JSON.stringify(inputJson))
+    const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+
+    if (confirmed) {
+      const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi())
+      const updateOperation = await inputParser.createEntityUpdateOperation(inputJson, 'Channel', channelId.toNumber())
+      await this.requestAccountDecoding(account)
+      this.log('Sending the extrinsic...')
+      await this.sendAndFollowNamedTx(account, 'contentDirectory', 'transaction', [actor, [updateOperation]], true)
+      saveOutputJson(output, `${inputJson.title}Channel.json`, inputJson)
+    }
+  }
+}

+ 82 - 0
cli/src/commands/media/updateVideo.ts

@@ -0,0 +1,82 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import VideoEntitySchema from 'cd-schemas/schemas/entities/VideoEntity.schema.json'
+import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
+import { LicenseEntity } from 'cd-schemas/types/entities/LicenseEntity'
+import { InputParser } from 'cd-schemas'
+import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
+import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
+
+export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
+  static description = 'Update existing video information (requires a membership).'
+  // TODO: Id as arg
+  static flags = {
+    // TODO: ...IOFlags, - providing input as json
+  }
+
+  async run() {
+    const account = await this.getRequiredSelectedAccount()
+    const memberId = await this.getRequiredMemberId()
+    const actor = { Member: memberId }
+
+    await this.requestAccountDecoding(account)
+
+    const [videoId, video] = await this.promptForEntityEntry('Select a video to update', 'Video', 'title', memberId)
+
+    const currentValues = await this.parseToKnownEntityJson<VideoEntity>(video)
+    const videoJsonSchema = (VideoEntitySchema as unknown) as JSONSchema
+
+    const { language: currLanguageId, category: currCategoryId, license: currLicenseId } = currentValues
+
+    const customizedPrompts: JsonSchemaCustomPrompts<VideoEntity> = [
+      [
+        'language',
+        () => this.promptForEntityId('Choose Video language', 'Language', 'name', undefined, currLanguageId),
+      ],
+      [
+        'category',
+        () => this.promptForEntityId('Choose Video category', 'ContentCategory', 'name', undefined, currCategoryId),
+      ],
+    ]
+    const videoPrompter = new JsonSchemaPrompter<VideoEntity>(videoJsonSchema, currentValues, customizedPrompts)
+
+    // Updating a license is currently a bit more tricky since it's a nested relation
+    const currKnownLicenseId = (await this.getAndParseKnownEntity<LicenseEntity>(currLicenseId)).knownLicense
+    const knownLicenseId = await this.promptForEntityId(
+      'Choose a license',
+      'KnownLicense',
+      'code',
+      undefined,
+      currKnownLicenseId
+    )
+    const updatedLicense: LicenseEntity = { knownLicense: knownLicenseId }
+
+    // Prompt for other video data
+    const updatedProps: Partial<VideoEntity> = await videoPrompter.promptMultipleProps([
+      'language',
+      'category',
+      'title',
+      'description',
+      'thumbnailURL',
+      'duration',
+      'isPublic',
+      'hasMarketing',
+    ])
+
+    this.jsonPrettyPrint(JSON.stringify(updatedProps))
+
+    // Parse inputs into operations and send final extrinsic
+    const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi())
+    const videoUpdateOperation = await inputParser.createEntityUpdateOperation(
+      updatedProps,
+      'Video',
+      videoId.toNumber()
+    )
+    const licenseUpdateOperation = await inputParser.createEntityUpdateOperation(
+      updatedLicense,
+      'License',
+      currentValues.license
+    )
+    const operations = [videoUpdateOperation, licenseUpdateOperation]
+    await this.sendAndFollowNamedTx(account, 'contentDirectory', 'transaction', [actor, operations], true)
+  }
+}

+ 347 - 0
cli/src/commands/media/uploadVideo.ts

@@ -0,0 +1,347 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import VideoEntitySchema from 'cd-schemas/schemas/entities/VideoEntity.schema.json'
+import VideoMediaEntitySchema from 'cd-schemas/schemas/entities/VideoMediaEntity.schema.json'
+import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
+import { VideoMediaEntity } from 'cd-schemas/types/entities/VideoMediaEntity'
+import { InputParser } from 'cd-schemas'
+import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
+import { JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
+import { flags } from '@oclif/command'
+import fs from 'fs'
+import ExitCodes from '../../ExitCodes'
+import { ContentId } from '@joystream/types/media'
+import ipfsHash from 'ipfs-only-hash'
+import { cli } from 'cli-ux'
+import axios, { AxiosRequestConfig } from 'axios'
+import { URL } from 'url'
+import ipfsHttpClient from 'ipfs-http-client'
+import first from 'it-first'
+import last from 'it-last'
+import toBuffer from 'it-to-buffer'
+import ffmpegInstaller from '@ffmpeg-installer/ffmpeg'
+import ffmpeg from 'fluent-ffmpeg'
+
+ffmpeg.setFfmpegPath(ffmpegInstaller.path)
+
+const DATA_OBJECT_TYPE_ID = 1
+const MAX_FILE_SIZE = 500 * 1024 * 1024
+
+type VideoMetadata = {
+  width?: number
+  height?: number
+  codecName?: string
+  codecFullName?: string
+  duration?: number
+}
+
+export default class UploadVideoCommand extends ContentDirectoryCommandBase {
+  static description = 'Upload a new Video to a channel (requires a membership).'
+  static flags = {
+    // TODO: ...IOFlags, - providing input as json
+    filePath: flags.string({
+      char: 'f',
+      required: true,
+      description: 'Path to the media file to upload',
+    }),
+  }
+
+  private createReadStreamWithProgressBar(filePath: string, barTitle: string, fileSize?: number) {
+    // Progress CLI UX:
+    // https://github.com/oclif/cli-ux#cliprogress
+    // https://www.npmjs.com/package/cli-progress
+    if (!fileSize) {
+      fileSize = fs.statSync(filePath).size
+    }
+    const progress = cli.progress({ format: `${barTitle} | {bar} | {value}/{total} KB processed` })
+    let processedKB = 0
+    const fileSizeKB = Math.ceil(fileSize / 1024)
+    progress.start(fileSizeKB, processedKB)
+    return {
+      fileStream: fs
+        .createReadStream(filePath)
+        .pause() // Explicitly pause to prevent switching to flowing mode (https://nodejs.org/api/stream.html#stream_event_data)
+        .on('error', () => {
+          progress.stop()
+          this.error(`Error while trying to read data from: ${filePath}!`, {
+            exit: ExitCodes.FsOperationFailed,
+          })
+        })
+        .on('data', (data) => {
+          processedKB += data.length / 1024
+          progress.update(processedKB)
+        })
+        .on('end', () => {
+          progress.update(fileSizeKB)
+          progress.stop()
+        }),
+      progressBar: progress,
+    }
+  }
+
+  private async calculateFileIpfsHash(filePath: string, fileSize: number): Promise<string> {
+    const { fileStream } = this.createReadStreamWithProgressBar(filePath, 'Calculating file hash', fileSize)
+    const hash: string = await ipfsHash.of(fileStream)
+
+    return hash
+  }
+
+  private async getDiscoveryDataViaLocalIpfsNode(ipnsIdentity: string): Promise<any> {
+    const ipfs = ipfsHttpClient({
+      // TODO: Allow customizing node url:
+      // host: 'localhost', port: '5001', protocol: 'http',
+      timeout: 10000,
+    })
+
+    const ipnsAddress = `/ipns/${ipnsIdentity}/`
+    const ipfsName = await last(
+      ipfs.name.resolve(ipnsAddress, {
+        recursive: false,
+        nocache: false,
+      })
+    )
+    const data: any = await first(ipfs.get(ipfsName))
+    const buffer = await toBuffer(data.content)
+
+    return JSON.parse(buffer.toString())
+  }
+
+  private async getDiscoveryDataViaBootstrapEndpoint(storageProviderId: number): Promise<any> {
+    const bootstrapEndpoint = await this.getApi().getRandomBootstrapEndpoint()
+    if (!bootstrapEndpoint) {
+      this.error('No bootstrap endpoints available', { exit: ExitCodes.ApiError })
+    }
+    this.log('Bootstrap endpoint:', bootstrapEndpoint)
+    const discoveryEndpoint = new URL(`/discover/v0/${storageProviderId}`, bootstrapEndpoint).toString()
+    try {
+      const data = (await axios.get(discoveryEndpoint)).data
+      return data
+    } catch (e) {
+      this.error(`Cannot retrieve data from bootstrap enpoint (${discoveryEndpoint})`, {
+        exit: ExitCodes.ExternalInfrastructureError,
+      })
+    }
+  }
+
+  private async getUploadUrlFromDiscoveryData(data: any, contentId: ContentId): Promise<string> {
+    if (typeof data === 'object' && data !== null && data.serialized) {
+      const unserialized = JSON.parse(data.serialized)
+      if (unserialized.asset && unserialized.asset.endpoint && typeof unserialized.asset.endpoint === 'string') {
+        return new URL(`/asset/v0/${contentId.encode()}`, unserialized.asset.endpoint).toString()
+      }
+    }
+    this.error(`Unexpected discovery data: ${JSON.stringify(data)}`)
+  }
+
+  private async getUploadUrl(ipnsIdentity: string, storageProviderId: number, contentId: ContentId): Promise<string> {
+    let data: any
+    try {
+      this.log('Trying to connect to local ipfs node...')
+      data = await this.getDiscoveryDataViaLocalIpfsNode(ipnsIdentity)
+    } catch (e) {
+      this.warn("Couldn't get data from local ipfs node, resolving to bootstrap endpoint...")
+      data = await this.getDiscoveryDataViaBootstrapEndpoint(storageProviderId)
+    }
+
+    const uploadUrl = await this.getUploadUrlFromDiscoveryData(data, contentId)
+
+    return uploadUrl
+  }
+
+  private async getVideoMetadata(filePath: string): Promise<VideoMetadata | null> {
+    let metadata: VideoMetadata | null = null
+    const metadataPromise = new Promise<VideoMetadata>((resolve, reject) => {
+      ffmpeg.ffprobe(filePath, (err, data) => {
+        if (err) {
+          reject(err)
+          return
+        }
+        const videoStream = data.streams.find((s) => s.codec_type === 'video')
+        if (videoStream) {
+          resolve({
+            width: videoStream.width,
+            height: videoStream.height,
+            codecName: videoStream.codec_name,
+            codecFullName: videoStream.codec_long_name,
+            duration: videoStream.duration !== undefined ? Math.ceil(Number(videoStream.duration)) || 0 : undefined,
+          })
+        } else {
+          reject(new Error('No video stream found in file'))
+        }
+      })
+    })
+
+    try {
+      metadata = await metadataPromise
+    } catch (e) {
+      const message = e.message || e
+      this.warn(`Failed to get video metadata via ffprobe (${message})`)
+    }
+
+    return metadata
+  }
+
+  private async uploadVideo(filePath: string, fileSize: number, uploadUrl: string) {
+    const { fileStream, progressBar } = this.createReadStreamWithProgressBar(filePath, 'Uploading', fileSize)
+    fileStream.on('end', () => {
+      cli.action.start('Waiting for the file to be processed...')
+    })
+
+    try {
+      const config: AxiosRequestConfig = {
+        headers: {
+          'Content-Type': '', // https://github.com/Joystream/storage-node-joystream/issues/16
+          'Content-Length': fileSize.toString(),
+        },
+        maxContentLength: MAX_FILE_SIZE,
+      }
+      await axios.put(uploadUrl, fileStream, config)
+      cli.action.stop()
+
+      this.log('File uploaded!')
+    } catch (e) {
+      progressBar.stop()
+      cli.action.stop()
+      const msg = (e.response && e.response.data && e.response.data.message) || e.message || e
+      this.error(`Unexpected error when trying to upload a file: ${msg}`, {
+        exit: ExitCodes.ExternalInfrastructureError,
+      })
+    }
+  }
+
+  async run() {
+    const account = await this.getRequiredSelectedAccount()
+    const memberId = await this.getRequiredMemberId()
+    const actor = { Member: memberId }
+
+    await this.requestAccountDecoding(account)
+
+    const { filePath } = this.parse(UploadVideoCommand).flags
+
+    // Basic file validation
+    if (!fs.existsSync(filePath)) {
+      this.error('File does not exist under provided path!', { exit: ExitCodes.FileNotFound })
+    }
+
+    const { size: fileSize } = fs.statSync(filePath)
+    if (fileSize > MAX_FILE_SIZE) {
+      this.error(`File size too large! Max. file size is: ${(MAX_FILE_SIZE / 1024 / 1024).toFixed(2)} MB`)
+    }
+
+    const videoMetadata = await this.getVideoMetadata(filePath)
+    this.log('Video media file parameters established:', { ...(videoMetadata || {}), size: fileSize })
+
+    // Start by prompting for a channel to make sure user has one available
+    const channelId = await this.promptForEntityId(
+      'Select a channel to publish the video under',
+      'Channel',
+      'title',
+      memberId
+    )
+
+    // Calculate hash and create content id
+    const contentId = ContentId.generate(this.getTypesRegistry())
+    const ipfsCid = await this.calculateFileIpfsHash(filePath, fileSize)
+
+    this.log('Video identification established:', {
+      contentId: contentId.toString(),
+      encodedContentId: contentId.encode(),
+      ipfsHash: ipfsCid,
+    })
+
+    // Send dataDirectory.addContent extrinsic
+    await this.sendAndFollowNamedTx(account, 'dataDirectory', 'addContent', [
+      memberId,
+      contentId,
+      DATA_OBJECT_TYPE_ID,
+      fileSize,
+      ipfsCid,
+    ])
+
+    const dataObject = await this.getApi().dataObjectByContentId(contentId)
+    if (!dataObject) {
+      this.error('Data object could not be retrieved from chain', { exit: ExitCodes.ApiError })
+    }
+
+    this.log('Data object:', dataObject.toJSON())
+
+    // Get storage provider identity
+    const storageProviderId = dataObject.liaison.toNumber()
+    const ipnsIdentity = await this.getApi().ipnsIdentity(storageProviderId)
+
+    if (!ipnsIdentity) {
+      this.error('Storage provider IPNS identity could not be determined', { exit: ExitCodes.ApiError })
+    }
+
+    // Resolve upload url and upload the video
+    const uploadUrl = await this.getUploadUrl(ipnsIdentity, storageProviderId, contentId)
+    this.log('Resolved upload url:', uploadUrl)
+
+    await this.uploadVideo(filePath, fileSize, uploadUrl)
+
+    // Prompting for the data:
+
+    // Set the defaults
+    const videoMediaDefaults: Partial<VideoMediaEntity> = {
+      pixelWidth: videoMetadata?.width,
+      pixelHeight: videoMetadata?.height,
+    }
+    const videoDefaults: Partial<VideoEntity> = {
+      duration: videoMetadata?.duration,
+    }
+    // Create prompting helpers
+    const videoJsonSchema = (VideoEntitySchema as unknown) as JSONSchema
+    const videoMediaJsonSchema = (VideoMediaEntitySchema as unknown) as JSONSchema
+
+    const videoMediaPrompter = new JsonSchemaPrompter<VideoMediaEntity>(videoMediaJsonSchema, videoMediaDefaults)
+    const videoPrompter = new JsonSchemaPrompter<VideoEntity>(videoJsonSchema, videoDefaults)
+
+    // Prompt for the data
+    const encodingSuggestion =
+      videoMetadata && videoMetadata.codecFullName ? ` (suggested: ${videoMetadata.codecFullName})` : ''
+    const encoding = await this.promptForEntityId(
+      `Choose Video encoding${encodingSuggestion}`,
+      'VideoMediaEncoding',
+      'name'
+    )
+    const { pixelWidth, pixelHeight } = await videoMediaPrompter.promptMultipleProps(['pixelWidth', 'pixelHeight'])
+    const language = await this.promptForEntityId('Choose Video language', 'Language', 'name')
+    const category = await this.promptForEntityId('Choose Video category', 'ContentCategory', 'name')
+    const license = await this.promptForEntityId('Choose License', 'KnownLicense', 'code')
+    const videoProps = await videoPrompter.promptMultipleProps([
+      'title',
+      'description',
+      'thumbnailURL',
+      'duration',
+      'isPublic',
+      'isExplicit',
+      'hasMarketing',
+    ])
+
+    // Create final inputs
+    const videoMediaInput: VideoMediaEntity = {
+      encoding,
+      pixelWidth,
+      pixelHeight,
+      size: fileSize,
+      location: { new: { joystreamMediaLocation: { new: { dataObjectId: contentId.encode() } } } },
+    }
+    const videoInput: VideoEntity = {
+      ...videoProps,
+      channel: channelId,
+      language,
+      category,
+      license: { new: { knownLicense: license } },
+      media: { new: videoMediaInput },
+    }
+
+    // Parse inputs into operations and send final extrinsic
+    const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi(), [
+      {
+        className: 'Video',
+        entries: [videoInput],
+      },
+    ])
+    const operations = await inputParser.getEntityBatchOperations()
+    await this.sendAndFollowNamedTx(account, 'contentDirectory', 'transaction', [actor, operations], true)
+  }
+}

+ 8 - 3
cli/src/helpers/InputOutput.ts

@@ -4,9 +4,13 @@ import ExitCodes from '../ExitCodes'
 import fs from 'fs'
 import path from 'path'
 import Ajv from 'ajv'
-import { JSONSchema7 } from 'json-schema'
+import $RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser'
+import { getSchemasLocation } from 'cd-schemas'
 import chalk from 'chalk'
 
+// Default schema path for resolving refs
+const DEFAULT_SCHEMA_PATH = getSchemasLocation('entities') + path.sep
+
 export const IOFlags = {
   input: flags.string({
     char: 'i',
@@ -20,7 +24,7 @@ export const IOFlags = {
   }),
 }
 
-export function getInputJson<T>(inputPath?: string, schema?: JSONSchema7): T | null {
+export async function getInputJson<T>(inputPath?: string, schema?: JSONSchema, schemaPath?: string): Promise<T | null> {
   if (inputPath) {
     let content, jsonObj
     try {
@@ -35,7 +39,8 @@ export function getInputJson<T>(inputPath?: string, schema?: JSONSchema7): T | n
     }
     if (schema) {
       const ajv = new Ajv()
-      const valid = ajv.validate(schema, jsonObj)
+      schema = await $RefParser.dereference(schemaPath || DEFAULT_SCHEMA_PATH, schema, {})
+      const valid = ajv.validate(schema, jsonObj) as boolean
       if (!valid) {
         throw new CLIError(`Input JSON file is not valid: ${ajv.errorsText()}`)
       }

+ 48 - 18
cli/src/helpers/JsonSchemaPrompt.ts

@@ -4,21 +4,36 @@ import _ from 'lodash'
 import RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import chalk from 'chalk'
 import { BOOL_PROMPT_OPTIONS } from './prompting'
+import { getSchemasLocation } from 'cd-schemas'
+import path from 'path'
 
 type CustomPromptMethod = () => Promise<any>
 type CustomPrompt = DistinctQuestion | CustomPromptMethod | { $item: CustomPrompt }
 
-export type JsonSchemaCustomPrompts = [string | RegExp, CustomPrompt][]
+// For the explaination of "string & { x: never }", see: https://github.com/microsoft/TypeScript/issues/29729
+// eslint-disable-next-line @typescript-eslint/ban-types
+export type JsonSchemaCustomPrompts<T = Record<string, unknown>> = [keyof T | (string & {}) | RegExp, CustomPrompt][]
+
+// Default schema path for resolving refs
+// TODO: Would be nice to skip the filename part (but without it it doesn't work)
+const DEFAULT_SCHEMA_PATH = getSchemasLocation('entities') + path.sep
 
 export class JsonSchemaPrompter<JsonResult> {
   schema: JSONSchema
+  schemaPath: string
   customPropmpts?: JsonSchemaCustomPrompts
   ajv: Ajv.Ajv
   filledObject: Partial<JsonResult>
 
-  constructor(schema: JSONSchema, defaults?: Partial<JsonResult>, customPrompts?: JsonSchemaCustomPrompts) {
+  constructor(
+    schema: JSONSchema,
+    defaults?: Partial<JsonResult>,
+    customPrompts?: JsonSchemaCustomPrompts,
+    schemaPath: string = DEFAULT_SCHEMA_PATH
+  ) {
     this.customPropmpts = customPrompts
     this.schema = schema
+    this.schemaPath = schemaPath
     this.ajv = new Ajv()
     this.filledObject = defaults || {}
   }
@@ -41,7 +56,7 @@ export class JsonSchemaPrompter<JsonResult> {
 
   private getCustomPrompt(propertyPath: string): CustomPrompt | undefined {
     const found = this.customPropmpts?.find(([pathToMatch]) =>
-      typeof pathToMatch === 'string' ? propertyPath === pathToMatch : pathToMatch.test(propertyPath)
+      pathToMatch instanceof RegExp ? pathToMatch.test(propertyPath) : propertyPath === pathToMatch
     )
 
     return found ? found[1] : undefined
@@ -51,8 +66,8 @@ export class JsonSchemaPrompter<JsonResult> {
     return chalk.green(propertyPath)
   }
 
-  private async prompt(schema: JSONSchema, propertyPath = ''): Promise<any> {
-    const customPrompt: CustomPrompt | undefined = this.getCustomPrompt(propertyPath)
+  private async prompt(schema: JSONSchema, propertyPath = '', custom?: CustomPrompt): Promise<any> {
+    const customPrompt: CustomPrompt | undefined = custom || this.getCustomPrompt(propertyPath)
     const propDisplayName = this.propertyDisplayName(propertyPath)
 
     // Custom prompt
@@ -83,9 +98,10 @@ export class JsonSchemaPrompter<JsonResult> {
     }
 
     // "primitive" values:
+    const currentValue = _.get(this.filledObject, propertyPath)
     const basicPromptOptions: DistinctQuestion = {
       message: propDisplayName,
-      default: _.get(this.filledObject, propertyPath) || schema.default,
+      default: currentValue !== undefined ? currentValue : schema.default,
     }
 
     let additionalPromptOptions: DistinctQuestion | undefined
@@ -110,7 +126,7 @@ export class JsonSchemaPrompter<JsonResult> {
     const promptOptions = { ...basicPromptOptions, ...additionalPromptOptions, ...customPrompt }
     // Need to wrap in retry, because "validate" will not get called if "type" is "list" etc.
     return await this.promptWithRetry(
-      async () => normalizer(await this.promptSimple(promptOptions, propertyPath, schema, normalizer)),
+      async () => normalizer(await this.promptSimple(promptOptions, propertyPath, normalizer)),
       propertyPath
     )
   }
@@ -153,12 +169,7 @@ export class JsonSchemaPrompter<JsonResult> {
     return result
   }
 
-  private async promptSimple(
-    promptOptions: DistinctQuestion,
-    propertyPath: string,
-    schema: JSONSchema,
-    normalize?: (v: any) => any
-  ) {
+  private async promptSimple(promptOptions: DistinctQuestion, propertyPath: string, normalize?: (v: any) => any) {
     const { result } = await inquirer.prompt([
       {
         ...promptOptions,
@@ -185,7 +196,8 @@ export class JsonSchemaPrompter<JsonResult> {
       error = this.setValueAndGetError(propertyPath, value, nestedErrors)
       if (error) {
         console.log('\n')
-        console.warn(error)
+        console.log('Provided value:', value)
+        console.warn(`ERROR: ${error}`)
         console.warn(`Try providing the input for ${propertyPath} again...`)
       }
     } while (error)
@@ -193,14 +205,32 @@ export class JsonSchemaPrompter<JsonResult> {
     return value
   }
 
+  async getMainSchema() {
+    return await RefParser.dereference(this.schemaPath, this.schema, {})
+  }
+
   async promptAll() {
-    await this.prompt(await RefParser.dereference(this.schema))
+    await this.prompt(await this.getMainSchema())
     return this.filledObject as JsonResult
   }
 
-  async promptSingleProp<P extends keyof JsonResult & string>(p: P): Promise<Exclude<JsonResult[P], undefined>> {
-    const dereferenced = await RefParser.dereference(this.schema)
-    await this.prompt(dereferenced.properties![p] as JSONSchema, p)
+  async promptMultipleProps<P extends keyof JsonResult & string, PA extends readonly P[]>(
+    props: PA
+  ): Promise<{ [K in PA[number]]: Exclude<JsonResult[K], undefined> }> {
+    const result: Partial<{ [K in PA[number]]: Exclude<JsonResult[K], undefined> }> = {}
+    for (const prop of props) {
+      result[prop] = await this.promptSingleProp(prop)
+    }
+
+    return result as { [K in PA[number]]: Exclude<JsonResult[K], undefined> }
+  }
+
+  async promptSingleProp<P extends keyof JsonResult & string>(
+    p: P,
+    customPrompt?: CustomPrompt
+  ): Promise<Exclude<JsonResult[P], undefined>> {
+    const mainSchema = await this.getMainSchema()
+    await this.prompt(mainSchema.properties![p] as JSONSchema, p, customPrompt)
     return this.filledObject[p] as Exclude<JsonResult[P], undefined>
   }
 }

+ 480 - 25
yarn.lock

@@ -1751,6 +1751,54 @@
     unique-filename "^1.1.1"
     which "^1.3.1"
 
+"@ffmpeg-installer/darwin-x64@4.1.0":
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz#48e1706c690e628148482bfb64acb67472089aaa"
+  integrity sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==
+
+"@ffmpeg-installer/ffmpeg@^1.0.20":
+  version "1.0.20"
+  resolved "https://registry.yarnpkg.com/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.0.20.tgz#d3c9c2bbcd76149468fb0886c2b3fe9e4795490b"
+  integrity sha512-wbgd//6OdwbFXYgV68ZyKrIcozEQpUKlvV66XHaqO2h3sFbX0jYLzx62Q0v8UcFWN21LoxT98NU2P+K0OWsKNA==
+  optionalDependencies:
+    "@ffmpeg-installer/darwin-x64" "4.1.0"
+    "@ffmpeg-installer/linux-arm" "4.1.3"
+    "@ffmpeg-installer/linux-arm64" "4.1.4"
+    "@ffmpeg-installer/linux-ia32" "4.1.0"
+    "@ffmpeg-installer/linux-x64" "4.1.0"
+    "@ffmpeg-installer/win32-ia32" "4.1.0"
+    "@ffmpeg-installer/win32-x64" "4.1.0"
+
+"@ffmpeg-installer/linux-arm64@4.1.4":
+  version "4.1.4"
+  resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz#7219f3f901bb67f7926cb060b56b6974a6cad29f"
+  integrity sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==
+
+"@ffmpeg-installer/linux-arm@4.1.3":
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz#c554f105ed5f10475ec25d7bec94926ce18db4c1"
+  integrity sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==
+
+"@ffmpeg-installer/linux-ia32@4.1.0":
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz#adad70b0d0d9d8d813983d6e683c5a338a75e442"
+  integrity sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==
+
+"@ffmpeg-installer/linux-x64@4.1.0":
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz#b4a5d89c4e12e6d9306dbcdc573df716ec1c4323"
+  integrity sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==
+
+"@ffmpeg-installer/win32-ia32@4.1.0":
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz#6eac4fb691b64c02e7a116c1e2d167f3e9b40638"
+  integrity sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==
+
+"@ffmpeg-installer/win32-x64@4.1.0":
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz#17e8699b5798d4c60e36e2d6326a8ebe5e95a2c5"
+  integrity sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==
+
 "@fortawesome/fontawesome-common-types@^0.2.30":
   version "0.2.30"
   resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.30.tgz#2f1cc5b46bd76723be41d0013a8450c9ba92b777"
@@ -2182,20 +2230,6 @@
     "@types/yargs" "^15.0.0"
     chalk "^4.0.0"
 
-"@joystream/types@^0.13.0":
-  version "0.13.1"
-  resolved "https://registry.yarnpkg.com/@joystream/types/-/types-0.13.1.tgz#d91773375a069663a3ec319f0929be8ddbb80210"
-  integrity sha512-Nz0zUzqS+gD0eaom+lzu595gm5XFG9uAK5L5ubwKceVDYgwVYC3rrZgfpKdPJJkGoGpVa8m0Gtl7YdOO1neMWw==
-  dependencies:
-    "@polkadot/api" "1.26.1"
-    "@polkadot/keyring" "^3.0.1"
-    "@polkadot/types" "1.26.1"
-    "@types/lodash" "^4.14.157"
-    "@types/vfile" "^4.0.0"
-    ajv "^6.11.0"
-    lodash "^4.17.15"
-    moment "^2.24.0"
-
 "@joystream/types@link:types":
   version "0.14.0"
   dependencies:
@@ -2969,6 +3003,11 @@
     call-me-maybe "^1.0.1"
     glob-to-regexp "^0.3.0"
 
+"@multiformats/base-x@^4.0.1":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121"
+  integrity sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==
+
 "@nodelib/fs.scandir@2.1.3":
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
@@ -3319,7 +3358,7 @@
     is-ipfs "^0.6.0"
     recursive-fs "^1.1.2"
 
-"@polkadot/api-contract@1.26.1", "@polkadot/api-contract@^1.26.1":
+"@polkadot/api-contract@^1.26.1":
   version "1.26.1"
   resolved "https://registry.yarnpkg.com/@polkadot/api-contract/-/api-contract-1.26.1.tgz#a8b52ef469ab8bbddb83191f8d451e31ffd76142"
   integrity sha512-zLGA/MHUJf12vanUEUBBRqpHVAONHWztoHS0JTIWUUS2+3GEXk6hGw+7PPtBDfDsLj0LgU/Qna1bLalC/zyl5w==
@@ -4404,6 +4443,13 @@
   resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3"
   integrity sha1-wFTor02d11205jq8dviFFocU1LM=
 
+"@types/fluent-ffmpeg@^2.1.16":
+  version "2.1.16"
+  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.16.tgz#63949b0cb6bc88c9157a579cdd80858a269f3a3a"
+  integrity sha512-1FTstm6xY/2WsJVt6ARV7CiJjNCQDlR/nfw6xuYk5ITbVjk7sw89Biyqm2DGW4c3aZ3vBx+5irZvsql4eybpoQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/fs-extra@^9.0.1":
   version "9.0.1"
   resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.1.tgz#91c8fc4c51f6d5dbe44c2ca9ab09310bd00c7918"
@@ -5468,6 +5514,13 @@ abbrev@1:
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
   integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
 
+abort-controller@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
+  integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+  dependencies:
+    event-target-shim "^5.0.0"
+
 abstract-leveldown@^6.2.1, abstract-leveldown@~6.2.1:
   version "6.2.2"
   resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.2.tgz#677425beeb28204367c7639e264e93ea4b49971a"
@@ -5874,6 +5927,13 @@ any-promise@^1.0.0, any-promise@^1.1.0:
   resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
   integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
 
+any-signal@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/any-signal/-/any-signal-1.2.0.tgz#d755f690896f3e75c4a07480f429a1ee7f8db3b4"
+  integrity sha512-Cl08k4xItix3jvu4cxO/dt2rQ6iUAjO66pTyRMub+WL1VXeAyZydCpD8GqWTPKfdL28U0R0UucmQVsUsBnvCmQ==
+  dependencies:
+    abort-controller "^3.0.0"
+
 anymatch@^1.3.0:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -6284,6 +6344,11 @@ async@0.9.x, async@^0.9.0:
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
   integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=
 
+async@>=0.2.9:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
+  integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
+
 async@^2.6.1, async@^2.6.2, async@^2.6.3:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
@@ -6952,6 +7017,15 @@ bl@^3.0.0:
   dependencies:
     readable-stream "^3.0.1"
 
+bl@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
+  integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==
+  dependencies:
+    buffer "^5.5.0"
+    inherits "^2.0.4"
+    readable-stream "^3.4.0"
+
 bl@^4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a"
@@ -6973,6 +7047,13 @@ blakejs@^1.1.0:
   resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.0.tgz#69df92ef953aa88ca51a32df6ab1c54a155fc7a5"
   integrity sha1-ad+S75U6qIylGjLfarHFShVfx6U=
 
+blob-to-it@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/blob-to-it/-/blob-to-it-0.0.2.tgz#851b4db0be4acebc86dd1c14c14b77fdc473e9b0"
+  integrity sha512-3/NRr0mUWQTkS71MYEC1teLbT5BTs7RZ6VMPXDV6qApjw3B4TAZspQuvDkYfHuD/XzL5p/RO91x5XRPeJvcCqg==
+  dependencies:
+    browser-readablestream-to-it "^0.0.2"
+
 block-stream@*:
   version "0.0.9"
   resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
@@ -6992,7 +7073,12 @@ bluebird@^3.1.1, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.4.0, bn.js@^5.1.2:
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.4.0:
+  version "4.11.9"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
+  integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
+
+bn.js@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0"
   integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==
@@ -7136,6 +7222,11 @@ browser-process-hrtime@^1.0.0:
   resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
   integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
 
+browser-readablestream-to-it@0.0.2, browser-readablestream-to-it@^0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/browser-readablestream-to-it/-/browser-readablestream-to-it-0.0.2.tgz#4a5c2a20567623c106125ca6b640f68b081cea25"
+  integrity sha512-bbiTccngeAbPmpTUJcUyr6JhivADKV9xkNJVLdA91vjdzXyFBZ6fgrzElQsV3k1UNGQACRTl3p4y+cEGG9U48A==
+
 browser-stdout@1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
@@ -7926,6 +8017,17 @@ cids@^0.5.7, cids@~0.5.2, cids@~0.5.4, cids@~0.5.5:
     multicodec "~0.5.0"
     multihashes "~0.4.14"
 
+cids@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cids/-/cids-1.0.1.tgz#119d50a686be06b51e972831bbf4149d9617c7cf"
+  integrity sha512-A5DvnNMr7ABdP3IWFPEdbpued8Sw2cHu6n4+Hke9MpzEqhQH74Pm6Zk2TB59aNHIuue1Oh/jZi/t0wgISWgOow==
+  dependencies:
+    class-is "^1.1.0"
+    multibase "^3.0.1"
+    multicodec "^2.0.1"
+    multihashes "^3.0.1"
+    uint8arrays "^1.1.0"
+
 cids@~0.7.0:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/cids/-/cids-0.7.1.tgz#d8bba49a35a0e82110879b5001abf1039c62347f"
@@ -8375,7 +8477,7 @@ columnify@^1.5.4:
     strip-ansi "^3.0.0"
     wcwidth "^1.0.0"
 
-combined-stream@^1.0.6, combined-stream@~1.0.6:
+combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
   integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@@ -11200,6 +11302,11 @@ event-emitter@^0.3.5:
     d "1"
     es5-ext "~0.10.14"
 
+event-target-shim@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
+  integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
 eventemitter3@^3.1.0:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
@@ -11576,6 +11683,11 @@ fast-diff@^1.1.2:
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
   integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
 
+fast-fifo@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.0.0.tgz#9bc72e6860347bb045a876d1c5c0af11e9b984e7"
+  integrity sha512-4VEXmjxLj7sbs8J//cn2qhRap50dGzF5n8fjay8mau+Jn4hxSeR3xPFwxMaQq/pDaq7+KQk0PAbC2+nWDkJrmQ==
+
 fast-glob@^2.0.2, fast-glob@^2.2.6:
   version "2.2.7"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
@@ -12018,6 +12130,14 @@ flow-remove-types@^2.112.0:
     pirates "^3.0.2"
     vlq "^0.2.1"
 
+fluent-ffmpeg@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz#c952de2240f812ebda0aa8006d7776ee2acf7d74"
+  integrity sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=
+  dependencies:
+    async ">=0.2.9"
+    which "^1.1.1"
+
 flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@@ -12113,6 +12233,15 @@ form-data@^2.3.1, form-data@^2.3.3:
     combined-stream "^1.0.6"
     mime-types "^2.1.12"
 
+form-data@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
+  integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 form-data@~2.3.2:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
@@ -14186,6 +14315,21 @@ ipfs-block@~0.8.0, ipfs-block@~0.8.1:
     cids "~0.7.0"
     class-is "^1.1.0"
 
+ipfs-core-utils@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/ipfs-core-utils/-/ipfs-core-utils-0.4.0.tgz#f633b0e51be6374f8caa1b15d5107e056123137a"
+  integrity sha512-IBPFvYjWPfVFpCeYUL/0gCUOabdBhh7aO5i4tU//UlF2gVCXPH4PRYlbBH9WM83zE2+o4vDi+dBXsdAI6nLPAg==
+  dependencies:
+    blob-to-it "0.0.2"
+    browser-readablestream-to-it "0.0.2"
+    cids "^1.0.0"
+    err-code "^2.0.0"
+    ipfs-utils "^3.0.0"
+    it-all "^1.0.1"
+    it-map "^1.0.2"
+    it-peekable "0.0.1"
+    uint8arrays "^1.1.0"
+
 ipfs-http-client@^32.0.1:
   version "32.0.1"
   resolved "https://registry.yarnpkg.com/ipfs-http-client/-/ipfs-http-client-32.0.1.tgz#4f5845c56717c748751e70e5d579b7b18af9e824"
@@ -14236,6 +14380,41 @@ ipfs-http-client@^32.0.1:
     tar-stream "^2.0.1"
     through2 "^3.0.1"
 
+ipfs-http-client@^47.0.1:
+  version "47.0.1"
+  resolved "https://registry.yarnpkg.com/ipfs-http-client/-/ipfs-http-client-47.0.1.tgz#509c6c742ab405bc2a7e6e0fe373e19e9b85633b"
+  integrity sha512-IAQf+uTLvXw5QFOzbyhu/5lH3rn7jEwwwdCGaNKVhoPI7yfyOV0wRse3hVWejjP1Id0P9mKuMKG8rhcY7pVAdQ==
+  dependencies:
+    abort-controller "^3.0.0"
+    any-signal "^1.1.0"
+    bignumber.js "^9.0.0"
+    cids "^1.0.0"
+    debug "^4.1.0"
+    form-data "^3.0.0"
+    ipfs-core-utils "^0.4.0"
+    ipfs-utils "^3.0.0"
+    ipld-block "^0.10.0"
+    ipld-dag-cbor "^0.17.0"
+    ipld-dag-pb "^0.20.0"
+    ipld-raw "^6.0.0"
+    iso-url "^0.4.7"
+    it-last "^1.0.2"
+    it-map "^1.0.2"
+    it-tar "^1.2.2"
+    it-to-buffer "^1.0.0"
+    it-to-stream "^0.1.1"
+    merge-options "^2.0.0"
+    multiaddr "^8.0.0"
+    multiaddr-to-uri "^6.0.0"
+    multibase "^3.0.0"
+    multicodec "^2.0.0"
+    multihashes "^3.0.1"
+    nanoid "^3.0.2"
+    node-fetch "^2.6.0"
+    parse-duration "^0.4.4"
+    stream-to-it "^0.2.1"
+    uint8arrays "^1.1.0"
+
 ipfs-only-hash@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/ipfs-only-hash/-/ipfs-only-hash-1.0.2.tgz#ef11c604d11b2504bc624fa473488a870326d38a"
@@ -14306,6 +14485,24 @@ ipfs-unixfs@~0.1.16:
   dependencies:
     protons "^1.0.1"
 
+ipfs-utils@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/ipfs-utils/-/ipfs-utils-3.0.0.tgz#58f8345ff26c4ae6b4a8e3a2366bd25de3e1460e"
+  integrity sha512-qahDc+fghrM57sbySr2TeWjaVR/RH/YEB/hvdAjiTbjESeD87qZawrXwj+19Q2LtGmFGusKNLo5wExeuI5ZfDQ==
+  dependencies:
+    abort-controller "^3.0.0"
+    any-signal "^1.1.0"
+    buffer "^5.6.0"
+    err-code "^2.0.0"
+    fs-extra "^9.0.1"
+    is-electron "^2.2.0"
+    iso-url "^0.4.7"
+    it-glob "0.0.8"
+    merge-options "^2.0.0"
+    nanoid "^3.1.3"
+    node-fetch "^2.6.0"
+    stream-to-it "^0.2.0"
+
 ipfs-utils@~0.0.3:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/ipfs-utils/-/ipfs-utils-0.0.4.tgz#946114cfeb6afb4454b4ccb10d2327cd323b0cce"
@@ -14319,6 +14516,26 @@ ipfs-utils@~0.0.3:
     kind-of "^6.0.2"
     readable-stream "^3.4.0"
 
+ipld-block@^0.10.0:
+  version "0.10.1"
+  resolved "https://registry.yarnpkg.com/ipld-block/-/ipld-block-0.10.1.tgz#a9de6185257cf56903cc7f71de450672f4871b65"
+  integrity sha512-lPMfW9tA2hVZw9hdO/YSppTxFmA0+5zxcefBOlCTOn+12RLyy+pdepKMbQw8u0KESFu3pYVmabNRWuFGcgHLLw==
+  dependencies:
+    cids "^1.0.0"
+    class-is "^1.1.0"
+
+ipld-dag-cbor@^0.17.0:
+  version "0.17.0"
+  resolved "https://registry.yarnpkg.com/ipld-dag-cbor/-/ipld-dag-cbor-0.17.0.tgz#760d15515275261d0da6b9d60bc387fb2866f068"
+  integrity sha512-YprSTQClJQUyC+RhbWrVXhg7ysII5R/jrmZZ4en4n9Mav+MRbntAW699zd1PHRLB71lNCJbxABE2Uc9QU2Ka7g==
+  dependencies:
+    borc "^2.1.2"
+    cids "^1.0.0"
+    is-circular "^1.0.2"
+    multicodec "^2.0.0"
+    multihashing-async "^2.0.0"
+    uint8arrays "^1.0.0"
+
 ipld-dag-cbor@~0.13.0:
   version "0.13.1"
   resolved "https://registry.yarnpkg.com/ipld-dag-cbor/-/ipld-dag-cbor-0.13.1.tgz#2ffecba3a13b29d8b604ee3d9b5dad0d4542e26b"
@@ -14344,6 +14561,21 @@ ipld-dag-cbor@~0.15.0:
     multicodec "^1.0.0"
     multihashing-async "~0.8.0"
 
+ipld-dag-pb@^0.20.0:
+  version "0.20.0"
+  resolved "https://registry.yarnpkg.com/ipld-dag-pb/-/ipld-dag-pb-0.20.0.tgz#025c0343aafe6cb9db395dd1dc93c8c60a669360"
+  integrity sha512-zfM0EdaolqNjAxIrtpuGKvXxWk5YtH9jKinBuQGTcngOsWFQhyybGCTJHGNGGtRjHNJi2hz5Udy/8pzv4kcKyg==
+  dependencies:
+    cids "^1.0.0"
+    class-is "^1.1.0"
+    multicodec "^2.0.0"
+    multihashing-async "^2.0.0"
+    protons "^2.0.0"
+    reset "^0.1.0"
+    run "^1.4.0"
+    stable "^0.1.8"
+    uint8arrays "^1.0.0"
+
 ipld-dag-pb@~0.15.2:
   version "0.15.3"
   resolved "https://registry.yarnpkg.com/ipld-dag-pb/-/ipld-dag-pb-0.15.3.tgz#0f72b99e77e3265f722457de5dc63b0f700fbb7d"
@@ -14389,6 +14621,15 @@ ipld-raw@^2.0.1:
     cids "~0.5.2"
     multihashing-async "~0.5.1"
 
+ipld-raw@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/ipld-raw/-/ipld-raw-6.0.0.tgz#74d947fcd2ce4e0e1d5bb650c1b5754ed8ea6da0"
+  integrity sha512-UK7fjncAzs59iu/o2kwYtb8jgTtW6B+cNWIiNpAJkfRwqoMk1xD/6i25ktzwe4qO8gQgoR9RxA5ibC23nq8BLg==
+  dependencies:
+    cids "^1.0.0"
+    multicodec "^2.0.0"
+    multihashing-async "^2.0.0"
+
 ipld@^0.20.2:
   version "0.20.2"
   resolved "https://registry.yarnpkg.com/ipld/-/ipld-0.20.2.tgz#1b4c007c361dd0426207d86305805ef8df79b763"
@@ -15128,6 +15369,11 @@ iso-639-1@^2.1.0:
   resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.1.4.tgz#c08b3d43a1c18945b05e26a257991ae6e36693ee"
   integrity sha512-pwJRHnpz1sCR5saQ+Hm1E2YESw2eLGKP5TzsYKXuQ7SIfvKWMRb9CHhptqunYpCIcRCpq3LgLuhYG5hiLPRbFQ==
 
+iso-constants@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/iso-constants/-/iso-constants-0.1.2.tgz#3d2456ed5aeaa55d18564f285ba02a47a0d885b4"
+  integrity sha512-OTCM5ZCQsHBCI4Wdu4tSxvDIkmDHd5EwJDps5mKqnQnWJSKlnwMs3EDZ4n3Fh1tmkWkDlyd2vCDbEYuPbyrUNQ==
+
 iso-random-stream@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/iso-random-stream/-/iso-random-stream-1.1.1.tgz#83824bba77fbb3480dd6b35fbb06de7f9e93e80f"
@@ -15145,16 +15391,16 @@ iso-stream-http@~0.1.2:
     inherits "^2.0.1"
     readable-stream "^3.1.1"
 
+iso-url@^0.4.7, iso-url@~0.4.6, iso-url@~0.4.7:
+  version "0.4.7"
+  resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-0.4.7.tgz#de7e48120dae46921079fe78f325ac9e9217a385"
+  integrity sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==
+
 iso-url@~0.4.4:
   version "0.4.6"
   resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-0.4.6.tgz#45005c4af4984cad4f8753da411b41b74cf0a8a6"
   integrity sha512-YQO7+aIe6l1aSJUKOx+Vrv08DlhZeLFIVfehG2L29KLSEb9RszqPXilxJRVpp57px36BddKR5ZsebacO5qG0tg==
 
-iso-url@~0.4.6, iso-url@~0.4.7:
-  version "0.4.7"
-  resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-0.4.7.tgz#de7e48120dae46921079fe78f325ac9e9217a385"
-  integrity sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==
-
 isobject@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
@@ -15289,6 +15535,89 @@ istanbul-reports@^3.0.2:
     html-escaper "^2.0.0"
     istanbul-lib-report "^3.0.0"
 
+it-all@^1.0.1, it-all@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/it-all/-/it-all-1.0.4.tgz#5a1aac996e2516c0d030911a631190b330afdb6d"
+  integrity sha512-7K+gjHHzZ7t+bCkrtulYiow35k3UgqH7miC+iUa9RGiyDRXJ6hVDeFsDrnWrlscjrkLFOJRKHxNOke4FNoQnhw==
+
+it-concat@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/it-concat/-/it-concat-1.0.1.tgz#4ac61c1b0a2185c97e1bd004d45f625902f7d728"
+  integrity sha512-ca7tnIqSpPycty9K+x08OwFj9kLSrsXgENn7ry2mNXlFlUkgEZe1/xvBjwnUlUEHvnITMj4Mq7ozPm1VaOm8FQ==
+  dependencies:
+    bl "^4.0.0"
+
+it-drain@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/it-drain/-/it-drain-1.0.3.tgz#2a3e6e667f65f5711faedb40ffb5358927609e93"
+  integrity sha512-KxwHBEpWW+0/EkGCOPR2MaHanvBW2A76tOC5CiitoJGLd8J56FxM6jJX3uow20v5qMidX5lnKgwH5oCIyYDszQ==
+
+it-first@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/it-first/-/it-first-1.0.4.tgz#359f2bf216686ec7498827991dc7fd503283b32b"
+  integrity sha512-L5ZB5k3Ol5ouAzLHo6fOCtByOy2lNjteNJpZLkE+VgmRt0MbC1ibmBW1AbOt6WzDx/QXFG5C8EEvY2nTXHg+Hw==
+
+it-glob@0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/it-glob/-/it-glob-0.0.8.tgz#b63d24945c18b35de8bb593a8c872fd0257c0cac"
+  integrity sha512-PmIAgb64aJPM6wwT1UTlNDAJnNgdGrvr0vRr3AYCngcUuq1KaAovuz0dQAmUkaXudDG3EQzc7OttuLW9DaL3YQ==
+  dependencies:
+    fs-extra "^8.1.0"
+    minimatch "^3.0.4"
+
+it-last@^1.0.2, it-last@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/it-last/-/it-last-1.0.4.tgz#4009aac79ee76e3417443c6c1dfb64cd380e9e5b"
+  integrity sha512-h0aV43BaD+1nubAKwStWcda6vlbejPSTQKfOrQvyNrrceluWfoq8DrBXnL0PSz6RkyHSiVSHtAEaqUijYMPo8Q==
+
+it-map@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/it-map/-/it-map-1.0.4.tgz#d413d2b0c3d8d9703df9e8a915ad96cb74a837ac"
+  integrity sha512-LZgYdb89XMo8cFUp6jF0cn5j3gF7wcZnKRVFS3qHHn0bPB2rpToh2vIkTBKduZLZxRRjWx1VW/udd98x+j2ulg==
+
+it-peekable@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/it-peekable/-/it-peekable-0.0.1.tgz#e3f91583d172444b9cd894ed2df6e26f0c176617"
+  integrity sha512-fd0JzbNldseeq+FFWthbqYB991UpKNyjPG6LqFhIOmJviCxSompMyoopKIXvLPLY+fBhhv2CT5PT31O/lEnTHw==
+
+it-reader@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/it-reader/-/it-reader-2.1.0.tgz#b1164be343f8538d8775e10fb0339f61ccf71b0f"
+  integrity sha512-hSysqWTO9Tlwc5EGjVf8JYZzw0D2FsxD/g+eNNWrez9zODxWt6QlN6JAMmycK72Mv4jHEKEXoyzUN4FYGmJaZw==
+  dependencies:
+    bl "^4.0.0"
+
+it-tar@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/it-tar/-/it-tar-1.2.2.tgz#8d79863dad27726c781a4bcc491f53c20f2866cf"
+  integrity sha512-M8V4a9I+x/vwXTjqvixcEZbQZHjwDIb8iUQ+D4M2QbhAdNs3WKVSl+45u5/F2XFx6jYMFOGzMVlKNK/uONgNIA==
+  dependencies:
+    bl "^4.0.0"
+    buffer "^5.4.3"
+    iso-constants "^0.1.2"
+    it-concat "^1.0.0"
+    it-reader "^2.0.0"
+    p-defer "^3.0.0"
+
+it-to-buffer@^1.0.0, it-to-buffer@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/it-to-buffer/-/it-to-buffer-1.0.4.tgz#4fcbd34c9c503e607744c0fdbeaff30008429703"
+  integrity sha512-wycpGeAdQ8WH8eSBkMHN/HMNiQ0Y88XEXo6s6LGJbQZjf9K7ppVzUfCXn7OnxFfUPN0HTWZr+uhthwtrwMTTfw==
+  dependencies:
+    buffer "^5.5.0"
+
+it-to-stream@^0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/it-to-stream/-/it-to-stream-0.1.2.tgz#7163151f75b60445e86b8ab1a968666acaacfe7b"
+  integrity sha512-DTB5TJRZG3untmZehcaFN0kGWl2bNv7tnJRgQHAO9QEt8jfvVRrebZtnD5NZd4SCj4WVPjl0LSrugNWE/UaZRQ==
+  dependencies:
+    buffer "^5.6.0"
+    fast-fifo "^1.0.0"
+    get-iterator "^1.0.2"
+    p-defer "^3.0.0"
+    p-fifo "^1.0.0"
+    readable-stream "^3.6.0"
+
 iterate-iterator@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6"
@@ -17643,6 +17972,13 @@ merge-options@^1.0.1:
   dependencies:
     is-plain-obj "^1.1"
 
+merge-options@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-2.0.0.tgz#36ca5038badfc3974dbde5e58ba89d3df80882c3"
+  integrity sha512-S7xYIeWHl2ZUKF7SDeBhGg6rfv5bKxVBdk95s/I7wVF8d+hjLSztJ/B271cnUiF6CAFduEQ5Zn3HYwAjT16DlQ==
+  dependencies:
+    is-plain-obj "^2.0.0"
+
 merge-source-map@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"
@@ -17846,7 +18182,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
 
-"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
+minimatch@*, "minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
@@ -18107,6 +18443,13 @@ multer@^1.4.1:
     type-is "^1.6.4"
     xtend "^4.0.0"
 
+multiaddr-to-uri@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/multiaddr-to-uri/-/multiaddr-to-uri-6.0.0.tgz#8f08a75c6eeb2370d5d24b77b8413e3f0fa9bcc0"
+  integrity sha512-OjpkVHOXEmIKMO8WChzzQ7aZQcSQX8squxmvtDbRpy7/QNmJ3Z7jv6qyD74C28QtaeNie8O8ngW2AkeiMmKP7A==
+  dependencies:
+    multiaddr "^8.0.0"
+
 multiaddr@^6.0.3, multiaddr@^6.0.4, multiaddr@^6.0.6, multiaddr@^6.1.0:
   version "6.1.1"
   resolved "https://registry.yarnpkg.com/multiaddr/-/multiaddr-6.1.1.tgz#9aae57b3e399089b9896d9455afa8f6b117dff06"
@@ -18131,6 +18474,18 @@ multiaddr@^7.2.1, multiaddr@^7.3.0:
     multibase "^0.7.0"
     varint "^5.0.0"
 
+multiaddr@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/multiaddr/-/multiaddr-8.0.0.tgz#22658c1811fd46fc26aaa66ed8c0de712e9a50ef"
+  integrity sha512-4OOyr0u0i4lvh9MY/mvuCNmH5eqoTamcnGeXz6umFGc0eaVQUGPDQNbp52YfFY92NlZ76pO6h4K2HkXsT5X43w==
+  dependencies:
+    cids "^1.0.0"
+    class-is "^1.1.0"
+    is-ip "^3.1.0"
+    multibase "^3.0.0"
+    uint8arrays "^1.1.0"
+    varint "^5.0.0"
+
 multibase@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/multibase/-/multibase-0.7.0.tgz#1adfc1c50abe05eefeb5091ac0c2728d6b84581b"
@@ -18147,6 +18502,14 @@ multibase@^1.0.0, multibase@^1.0.1:
     base-x "^3.0.8"
     buffer "^5.5.0"
 
+multibase@^3.0.0, multibase@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/multibase/-/multibase-3.0.1.tgz#31e8b0de5b2fd5f7dc9f04dddf0a4fcc667284bf"
+  integrity sha512-MRU5WpnSg81/vYO977MweoeUAxBdXl7+F5Af2Es+X6Vcgfk/g/EjIqXTgm3kb+xO3m1Kzr+aIV14oRX7nv5Z9w==
+  dependencies:
+    "@multiformats/base-x" "^4.0.1"
+    web-encoding "^1.0.2"
+
 multibase@~0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/multibase/-/multibase-0.6.0.tgz#0216e350614c7456da5e8e5b20d3fcd4c9104f56"
@@ -18175,6 +18538,14 @@ multicodec@^1.0.0, multicodec@^1.0.1:
     buffer "^5.6.0"
     varint "^5.0.0"
 
+multicodec@^2.0.0, multicodec@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-2.0.1.tgz#0971bbef83fcb354315c837c9a3f3e2e422af371"
+  integrity sha512-YDYeWn9iGa76hOHAyyZa0kbt3tr5FLg1ZXUHrZUJltjnxxdbTIbHnxWLd2zTcMOjdT3QyO+Xs4bQgJUcC2RWUA==
+  dependencies:
+    uint8arrays "1.0.0"
+    varint "^5.0.0"
+
 multicodec@~0.5.0, multicodec@~0.5.1:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-0.5.5.tgz#55c2535b44eca9ea40a13771420153fe075bb36d"
@@ -18182,6 +18553,15 @@ multicodec@~0.5.0, multicodec@~0.5.1:
   dependencies:
     varint "^5.0.0"
 
+multihashes@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-3.0.1.tgz#607c243d5e04ec022ac76c9c114e08416216f019"
+  integrity sha512-fFY67WOtb0359IjDZxaCU3gJILlkwkFbxbwrK9Bej5+NqNaYztzLOj8/NgMNMg/InxmhK+Uu8S/U4EcqsHzB7Q==
+  dependencies:
+    multibase "^3.0.0"
+    uint8arrays "^1.0.0"
+    varint "^5.0.0"
+
 multihashes@~0.4.13, multihashes@~0.4.14:
   version "0.4.15"
   resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-0.4.15.tgz#6dbc55f7f312c6782f5367c03c9783681589d8a6"
@@ -18199,6 +18579,18 @@ multihashes@~0.4.15, multihashes@~0.4.19:
     multibase "^1.0.1"
     varint "^5.0.0"
 
+multihashing-async@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/multihashing-async/-/multihashing-async-2.0.1.tgz#cc50e05e88b02ed0a2d8a9518d8a6cf1fcf12aa1"
+  integrity sha512-LZcH8PqW4iEKymaJ3RpsgpSJhXF29kAvO02ccqbysiXkQhZpVce8rrg+vzRKWO89hhyIBnQHI2e/ZoRVxmiJ2Q==
+  dependencies:
+    blakejs "^1.1.0"
+    err-code "^2.0.0"
+    js-sha3 "^0.8.0"
+    multihashes "^3.0.1"
+    murmurhash3js-revisited "^3.0.0"
+    uint8arrays "^1.0.0"
+
 multihashing-async@~0.5.1:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/multihashing-async/-/multihashing-async-0.5.2.tgz#4af40e0dde2f1dbb12a7c6b265181437ac26b9de"
@@ -18299,7 +18691,7 @@ nan@^2.13.2:
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
   integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
 
-nanoid@^3.1.3:
+nanoid@^3.0.2, nanoid@^3.1.3:
   version "3.1.12"
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.12.tgz#6f7736c62e8d39421601e4a0c77623a97ea69654"
   integrity sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==
@@ -19387,6 +19779,14 @@ p-each-series@^2.1.0:
   resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
   integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==
 
+p-fifo@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-fifo/-/p-fifo-1.0.0.tgz#e29d5cf17c239ba87f51dde98c1d26a9cfe20a63"
+  integrity sha512-IjoCxXW48tqdtDFz6fqo5q1UfFVjjVZe8TC1QRflvNUJtNfCUhxOUw6MOVZhDPjqhSzc26xKdugsO17gmzd5+A==
+  dependencies:
+    fast-fifo "^1.0.0"
+    p-defer "^3.0.0"
+
 p-finally@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@@ -19596,6 +19996,11 @@ parse-asn1@^5.0.0:
     pbkdf2 "^3.0.3"
     safe-buffer "^5.1.1"
 
+parse-duration@^0.4.4:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-0.4.4.tgz#11c0f51a689e97d06c57bd772f7fda7dc013243c"
+  integrity sha512-KbAJuYGUhZkB9gotDiKLnZ7Z3VTacK3fgwmDdB6ZVDtJbMBT6MfLga0WJaYpPDu0mzqT0NgHtHDt5PY4l0nidg==
+
 parse-entities@^1.0.2, parse-entities@^1.1.0, parse-entities@^1.1.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50"
@@ -20799,6 +21204,16 @@ protons@^1.0.1:
     signed-varint "^2.0.1"
     varint "^5.0.0"
 
+protons@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/protons/-/protons-2.0.0.tgz#a6910161f0ed0f3a9007c1503acda7a402023cd8"
+  integrity sha512-BTrE9D6/d1NGis+0D8TqAO1THdn4evHQhfjapA0NUaRH4+ecJJcbqaF7TE/DKv5czE9VB/TeOllBOmCyJhHnhg==
+  dependencies:
+    protocol-buffers-schema "^3.3.1"
+    signed-varint "^2.0.1"
+    uint8arrays "^1.0.0"
+    varint "^5.0.0"
+
 proxy-addr@~2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
@@ -22372,6 +22787,11 @@ reselect@^4.0.0:
   resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
   integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
 
+reset@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/reset/-/reset-0.1.0.tgz#9fc7314171995ae6cb0b7e58b06ce7522af4bafb"
+  integrity sha1-n8cxQXGZWubLC35YsGznUir0uvs=
+
 resize-observer-polyfill@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
@@ -22603,6 +23023,13 @@ run-queue@^1.0.0, run-queue@^1.0.3:
   dependencies:
     aproba "^1.1.1"
 
+run@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/run/-/run-1.4.0.tgz#e17d9e9043ab2fe17776cb299e1237f38f0b4ffa"
+  integrity sha1-4X2ekEOrL+F3dsspnhI3848LT/o=
+  dependencies:
+    minimatch "*"
+
 rxjs-compat@^6.6.0:
   version "6.6.2"
   resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.6.2.tgz#23592564243cf24641a5d2e2d2acfc8f6b127186"
@@ -23750,6 +24177,13 @@ stream-shift@^1.0.0:
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
   integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
 
+stream-to-it@^0.2.0, stream-to-it@^0.2.1:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/stream-to-it/-/stream-to-it-0.2.2.tgz#fb3de7917424c354a987c7bc2aab2d0facbd7d94"
+  integrity sha512-waULBmQpVdr6TkDzci6t1P7dIaSZ0bHC1TaPXDUeJC5PpSK7U3T0H0Zeo/LWUnd6mnhXOmGGDKAkjUCHw5IOng==
+  dependencies:
+    get-iterator "^1.0.2"
+
 stream-to-pull-stream@^1.7.2:
   version "1.7.3"
   resolved "https://registry.yarnpkg.com/stream-to-pull-stream/-/stream-to-pull-stream-1.7.3.tgz#4161aa2d2eb9964de60bfa1af7feaf917e874ece"
@@ -25355,6 +25789,22 @@ uid-number@0.0.6:
   resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
   integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
 
+uint8arrays@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-1.0.0.tgz#9cf979517f85c32d6ef54adf824e3499bb715331"
+  integrity sha512-14tqEVujDREW7YwonSZZwLvo7aFDfX7b6ubvM/U7XvZol+CC/LbhaX/550VlWmhddAL9Wou1sxp0Of3tGqXigg==
+  dependencies:
+    multibase "^3.0.0"
+    web-encoding "^1.0.2"
+
+uint8arrays@^1.0.0, uint8arrays@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-1.1.0.tgz#d034aa65399a9fd213a1579e323f0b29f67d0ed2"
+  integrity sha512-cLdlZ6jnFczsKf5IH1gPHTtcHtPGho5r4CvctohmQjw8K7Q3gFdfIGHxSTdTaCKrL4w09SsPRJTqRS0drYeszA==
+  dependencies:
+    multibase "^3.0.0"
+    web-encoding "^1.0.2"
+
 umask@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
@@ -26263,6 +26713,11 @@ wcwidth@^1.0.0, wcwidth@^1.0.1:
   dependencies:
     defaults "^1.0.3"
 
+web-encoding@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/web-encoding/-/web-encoding-1.0.4.tgz#0398d39ce2cbef5ed2617080750ed874e6153aea"
+  integrity sha512-DcXs2lbVPzuJmn2kuDEwul2oZg7p4YMa5J2f0YzsOBHaAnBYGPNUB/rJ74DTjTKpw7F0+lSsVM8sFHE2UyBixg==
+
 webidl-conversions@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -26588,7 +27043,7 @@ which-pm-runs@^1.0.0:
   resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
   integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
 
-which@1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
+which@1, which@^1.1.1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==