diff --git a/src/core/batch.ts b/src/core/batch.ts index e4335ff5..3102ac3b 100644 --- a/src/core/batch.ts +++ b/src/core/batch.ts @@ -6,7 +6,7 @@ import { Node, NodeLike, NodeType, Stack } from "../types/node"; import { FileLike } from "../types/file"; import { BatchMembershipInviteResponse, BatchStackCreateResponse } from "../types/batch-response"; import { Membership, RoleType } from "../types/membership"; -import { Hooks } from "./file"; +import { FileService, Hooks } from "./file"; import { actionRefs, functions, objectType, protocolTags } from "../constants"; import { ContractInput, Tag, Tags } from "../types/contract"; import { ObjectType } from "../types/object"; @@ -152,7 +152,6 @@ class BatchService extends Service { // upload file data & metadata Promise.all(chunk.map(async (item) => { const service = new StackService(this.wallet, this.api, this); - service.setVaultContextForFile(); const nodeId = uuidv4(); service.setObjectId(nodeId); @@ -165,9 +164,13 @@ class BatchService extends Service { service.setParentId(createOptions.parentId); service.arweaveTags = await service.getTxTags(); + const fileService = new FileService(this.wallet, this.api, service); + const fileUploadResult = await fileService.create(item.file, createOptions); + const version = await fileService.newVersion(item.file, fileUploadResult); + const state = { name: await service.processWriteString(item.name ? item.name : item.file.name), - versions: [await service.uploadNewFileVersion(item.file, createOptions)], + versions: [version], tags: service.tags }; const id = await service.uploadState(state); diff --git a/src/core/common.ts b/src/core/common.ts new file mode 100644 index 00000000..eaa866d9 --- /dev/null +++ b/src/core/common.ts @@ -0,0 +1,59 @@ +import { ListOptions } from "../types/query-options"; +import lodash from "lodash"; +import { EncryptionMetadata } from "./service"; +import { EncryptedPayload } from "@akord/crypto/lib/types"; +import { base64ToArray } from "@akord/crypto"; + +export const handleListErrors = async (originalItems: Array, promises: Array>) + : Promise<{ items: Array, errors: Array<{ id: string, error: Error }> }> => { + const results = await Promise.all(promises.map(p => p.catch(e => e))); + const items = results.filter(result => !(result instanceof Error)); + const errors = results + .map((result, index) => ({ result, index })) + .filter((mapped) => mapped.result instanceof Error) + .map((filtered) => ({ id: (originalItems[filtered.index]).id, error: filtered.result })); + return { items, errors }; +} + +export const paginate = async (apiCall: any, listOptions: ListOptions & { vaultId?: string }): Promise> => { + let token = undefined; + let results = [] as T[]; + do { + const { items, nextToken } = await apiCall(listOptions); + results = results.concat(items); + token = nextToken; + listOptions.nextToken = nextToken; + if (nextToken === "null") { + token = undefined; + } + } while (token); + return results; +} + +export const mergeState = (currentState: any, stateUpdates: any): any => { + let newState = lodash.cloneDeepWith(currentState); + lodash.mergeWith( + newState, + stateUpdates, + function concatArrays(objValue, srcValue) { + if (lodash.isArray(objValue)) { + return objValue.concat(srcValue); + } + }); + return newState; +} + +export const getEncryptedPayload = (data: ArrayBuffer | string, metadata: EncryptionMetadata) + : EncryptedPayload => { + const { encryptedKey, iv } = metadata; + if (encryptedKey && iv) { + return { + encryptedKey, + encryptedData: { + iv: base64ToArray(iv), + ciphertext: data as ArrayBuffer + } + } + } + return null; +} diff --git a/src/core/contract.ts b/src/core/contract.ts index c773f037..aa0047f4 100644 --- a/src/core/contract.ts +++ b/src/core/contract.ts @@ -11,7 +11,6 @@ class ContractService extends Service { */ public async getState(id: string): Promise { const contract = await this.api.getContractState(id); - this.setIsPublic(contract.public); if (contract.public) { return contract; } else { diff --git a/src/core/file.ts b/src/core/file.ts index cb0216c2..6ede2ae0 100644 --- a/src/core/file.ts +++ b/src/core/file.ts @@ -11,6 +11,7 @@ import { BinaryLike } from "crypto"; import { getTxData, getTxMetadata } from "../arweave"; import * as mime from "mime-types"; import { CONTENT_TYPE as MANIFEST_CONTENT_TYPE, FILE_TYPE as MANIFEST_FILE_TYPE } from "./manifest"; +import { FileVersion } from "../types/node"; const DEFAULT_FILE_TYPE = "text/plain"; @@ -28,27 +29,27 @@ class FileService extends Service { * @returns Promise with file buffer */ public async get(id: string, vaultId: string, options: DownloadOptions = {}): Promise { - await this.setVaultContext(vaultId); + const service = new FileService(this.wallet, this.api); + await service.setVaultContext(vaultId); const downloadOptions = options as FileDownloadOptions; - downloadOptions.public = this.isPublic; + downloadOptions.public = service.isPublic; let fileBinary: ArrayBuffer; if (options.isChunked) { let currentChunk = 0; while (currentChunk < options.numberOfChunks) { const url = `${id}_${currentChunk}`; downloadOptions.loadedSize = currentChunk * this.chunkSize; - const chunkBinary = await this.getBinary(url, downloadOptions); - fileBinary = this.appendBuffer(fileBinary, chunkBinary); + const chunkBinary = await service.getBinary(url, downloadOptions); + fileBinary = service.appendBuffer(fileBinary, chunkBinary); currentChunk++; } } else { const { fileData, metadata } = await this.api.downloadFile(id, downloadOptions); - fileBinary = await this.processReadRaw(fileData, metadata) + fileBinary = await service.processReadRaw(fileData, metadata) } return fileBinary; } - /** * Downloads the file keeping memory consumed (RAM) under defiend level: this#chunkSize. * In browser, streaming of the binary requires self hosting of mitm.html and sw.js @@ -59,17 +60,18 @@ class FileService extends Service { * @returns Promise with file buffer */ public async download(id: string, vaultId: string, options: DownloadOptions = {}): Promise { - await this.setVaultContext(vaultId); + const service = new FileService(this.wallet, this.api); + await service.setVaultContext(vaultId); const downloadOptions = options as FileDownloadOptions; - downloadOptions.public = this.isPublic; - const writer = await this.stream(options.name, options.resourceSize); + downloadOptions.public = service.isPublic; + const writer = await service.stream(options.name, options.resourceSize); if (options.isChunked) { let currentChunk = 0; try { while (currentChunk < options.numberOfChunks) { const url = `${id}_${currentChunk}`; - downloadOptions.loadedSize = currentChunk * this.chunkSize; - const fileBinary = await this.getBinary(url, downloadOptions); + downloadOptions.loadedSize = currentChunk * service.chunkSize; + const fileBinary = await service.getBinary(url, downloadOptions); if (writer instanceof WritableStreamDefaultWriter) { await writer.ready } @@ -85,7 +87,7 @@ class FileService extends Service { await writer.close(); } } else { - const fileBinary = await this.getBinary(id, downloadOptions); + const fileBinary = await service.getBinary(id, downloadOptions); await writer.write(new Uint8Array(fileBinary)); await writer.close(); } @@ -157,6 +159,24 @@ class FileService extends Service { } } + public async newVersion(file: FileLike, uploadResult: FileUploadResult): Promise { + const version = new FileVersion({ + owner: await this.wallet.getAddress(), + createdAt: JSON.stringify(Date.now()), + name: await this.processWriteString(file.name), + type: file.type, + size: file.size, + resourceUri: [ + `arweave:${uploadResult.resourceTx}`, + `hash:${uploadResult.resourceHash}`, + `s3:${uploadResult.resourceUrl}` + ], + numberOfChunks: uploadResult.numberOfChunks, + chunkSize: uploadResult.chunkSize, + }); + return version; + } + private retrieveFileMetadata(fileTxId: string, tags: Tags = []) : { name: string, type: string } { const type = this.retrieveFileType(tags); diff --git a/src/core/folder.ts b/src/core/folder.ts index 14bd0d5f..2541d77b 100644 --- a/src/core/folder.ts +++ b/src/core/folder.ts @@ -13,15 +13,16 @@ class FolderService extends NodeService { * @returns Promise with new folder id & corresponding transaction id */ public async create(vaultId: string, name: string, options: NodeCreateOptions = this.defaultCreateOptions): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.FOLDER_CREATE); - this.setFunction(functions.NODE_CREATE); - this.setAkordTags((this.isPublic ? [name] : []).concat(options.tags)); + const service = new FolderService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.FOLDER_CREATE); + service.setFunction(functions.NODE_CREATE); + service.setAkordTags((service.isPublic ? [name] : []).concat(options.tags)); const state = { - name: await this.processWriteString(name), + name: await service.processWriteString(name), tags: options.tags || [] } - const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); + const { nodeId, transactionId, object } = await service.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { folderId: nodeId, transactionId, object }; } }; diff --git a/src/core/membership.ts b/src/core/membership.ts index 5a4fd6b5..3d11c5d5 100644 --- a/src/core/membership.ts +++ b/src/core/membership.ts @@ -1,6 +1,6 @@ import { actionRefs, objectType, status, functions, protocolTags } from "../constants"; import { v4 as uuidv4 } from "uuid"; -import { EncryptedKeys, Encrypter, deriveAddress, base64ToArray } from "@akord/crypto"; +import { EncryptedKeys, Encrypter, deriveAddress, base64ToArray, generateKeyPair, Keys } from "@akord/crypto"; import { Service } from "./service"; import { Membership, RoleType, StatusType } from "../types/membership"; import { GetOptions, ListOptions } from "../types/query-options"; @@ -8,7 +8,9 @@ import { MembershipInput, Tag, Tags } from "../types/contract"; import { Paginated } from "../types/paginated"; import { BadRequest } from "../errors/bad-request"; import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; -import { UserPublicInfo } from "../types"; +import { handleListErrors, paginate } from "./common"; +import { ProfileService } from "./profile"; +import { ProfileDetails } from "../types/profile-details"; export const activeStatus = [status.ACCEPTED, status.PENDING, status.INVITED] as StatusType[]; @@ -61,7 +63,7 @@ class MembershipService extends Service { .map(async (membershipProto: Membership) => { return await this.processMembership(membershipProto, !membershipProto.__public__ && listOptions.shouldDecrypt, membershipProto.__keys__); }) as Promise[]; - const { items, errors } = await this.handleListErrors(response.items, promises); + const { items, errors } = await handleListErrors(response.items, promises); return { items, nextToken: response.nextToken, @@ -78,7 +80,7 @@ class MembershipService extends Service { const list = async (options: ListOptions & { vaultId: string }) => { return await this.list(options.vaultId, options); } - return await this.paginate(list, { ...options, vaultId }); + return await paginate(list, { ...options, vaultId }); } /** @@ -90,37 +92,38 @@ class MembershipService extends Service { * @returns Promise with new membership id & corresponding transaction id */ public async invite(vaultId: string, email: string, role: RoleType, options: MembershipCreateOptions = {}): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.MEMBERSHIP_INVITE); - this.setFunction(functions.MEMBERSHIP_INVITE); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.MEMBERSHIP_INVITE); + service.setFunction(functions.MEMBERSHIP_INVITE); const membershipId = uuidv4(); - this.setObjectId(membershipId); + service.setObjectId(membershipId); const { address, publicKey, publicSigningKey } = await this.api.getUserPublicData(email); const state = { - keys: await this.prepareMemberKeys(publicKey), - encPublicSigningKey: await this.processWriteString(publicSigningKey) + keys: await service.prepareMemberKeys(publicKey), + encPublicSigningKey: await service.processWriteString(publicSigningKey) }; - this.arweaveTags = [new Tag(protocolTags.MEMBER_ADDRESS, address)] - .concat(await this.getTxTags()); + service.arweaveTags = [new Tag(protocolTags.MEMBER_ADDRESS, address)] + .concat(await service.getTxTags()); - const dataTxId = await this.uploadState(state); + const dataTxId = await service.uploadState(state); const input = { - function: this.function, + function: service.function, address, role, data: dataTxId } const { id, object } = await this.api.postContractTransaction( - this.vaultId, + service.vaultId, input, - this.arweaveTags, + service.arweaveTags, { message: options.message } ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { membershipId, transactionId: id, object: membership }; } @@ -137,28 +140,29 @@ class MembershipService extends Service { transactionId: string, members: Array<{ id: string, address: string }> }> { - await this.setVaultContext(vaultId); - this.setActionRef("MEMBERSHIP_AIRDROP"); - this.setFunction(functions.MEMBERSHIP_ADD); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef("MEMBERSHIP_AIRDROP"); + service.setFunction(functions.MEMBERSHIP_ADD); const memberArray = [] as MembershipInput[]; const membersMetadata = []; const dataArray = [] as { id: string, data: string }[]; const memberTags = [] as Tags; for (const member of members) { const membershipId = uuidv4(); - this.setObjectId(membershipId); + service.setObjectId(membershipId); const memberAddress = await deriveAddress(base64ToArray(member.publicSigningKey)); const state = { id: membershipId, address: memberAddress, - keys: await this.prepareMemberKeys(member.publicKey), - encPublicSigningKey: await this.processWriteString(member.publicSigningKey), - memberDetails: await this.processMemberDetails({ name: member.options?.name }, this.vault.cacheOnly), + keys: await service.prepareMemberKeys(member.publicKey), + encPublicSigningKey: await service.processWriteString(member.publicSigningKey), + memberDetails: await service.processMemberDetails({ name: member.options?.name }, service.vault.cacheOnly), }; - const data = await this.uploadState(state); + const data = await service.uploadState(state); dataArray.push({ id: membershipId, data @@ -174,17 +178,17 @@ class MembershipService extends Service { memberTags.push(new Tag(protocolTags.MEMBERSHIP_ID, membershipId)); } - this.arweaveTags = memberTags.concat(await super.getTxTags()); + service.arweaveTags = memberTags.concat(await super.getTxTags()); const input = { - function: this.function, + function: service.function, members: memberArray }; const { id } = await this.api.postContractTransaction( - this.vaultId, + service.vaultId, input, - this.arweaveTags, + service.arweaveTags, { members: membersMetadata } ); return { members: input.members, transactionId: id }; @@ -195,22 +199,24 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async accept(membershipId: string): Promise { - const memberDetails = await this.getProfileDetails(); - await this.setVaultContextFromMembershipId(membershipId); + const profileService = new ProfileService(this.wallet, this.api); + const memberDetails = await profileService.get(); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); const state = { - memberDetails: await this.processMemberDetails(memberDetails, this.object.__cacheOnly__), - encPublicSigningKey: await this.processWriteString(this.wallet.signingPublicKey()) + memberDetails: await service.processMemberDetails(memberDetails, service.object.__cacheOnly__), + encPublicSigningKey: await service.processWriteString(this.wallet.signingPublicKey()) } - this.setActionRef(actionRefs.MEMBERSHIP_ACCEPT); - this.setFunction(functions.MEMBERSHIP_ACCEPT); + service.setActionRef(actionRefs.MEMBERSHIP_ACCEPT); + service.setFunction(functions.MEMBERSHIP_ACCEPT); - const data = await this.mergeAndUploadState(state); + const data = await service.mergeAndUploadState(state); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - await this.getTxTags() + service.vaultId, + { function: service.function, data }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -219,34 +225,35 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async confirm(membershipId: string): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_CONFIRM); - this.setFunction(functions.MEMBERSHIP_INVITE); - const { address, publicKey, publicSigningKey } = await this.api.getUserPublicData(this.object.email); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_CONFIRM); + service.setFunction(functions.MEMBERSHIP_INVITE); + const { address, publicKey, publicSigningKey } = await this.api.getUserPublicData(service.object.email); const state = { - keys: await this.prepareMemberKeys(publicKey), - encPublicSigningKey: await this.processWriteString(publicSigningKey) + keys: await service.prepareMemberKeys(publicKey), + encPublicSigningKey: await service.processWriteString(publicSigningKey) }; - this.arweaveTags = [new Tag(protocolTags.MEMBER_ADDRESS, address)] - .concat(await this.getTxTags()); + service.arweaveTags = [new Tag(protocolTags.MEMBER_ADDRESS, address)] + .concat(await service.getTxTags()); - const dataTxId = await this.uploadState(state); + const dataTxId = await service.uploadState(state); const input = { - function: this.function, + function: service.function, address, data: dataTxId, - role: this.object.role + role: service.object.role } const { id, object } = await this.api.postContractTransaction( - this.vaultId, + service.vaultId, input, - this.arweaveTags + service.arweaveTags ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -255,16 +262,17 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async reject(membershipId: string): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_REJECT); - this.setFunction(functions.MEMBERSHIP_REJECT); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_REJECT); + service.setFunction(functions.MEMBERSHIP_REJECT); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function }, - await this.getTxTags() + service.vaultId, + { function: service.function }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -273,16 +281,17 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async leave(membershipId: string): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_LEAVE); - this.setFunction(functions.MEMBERSHIP_REJECT); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_LEAVE); + service.setFunction(functions.MEMBERSHIP_REJECT); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function }, - await this.getTxTags() + service.vaultId, + { function: service.function }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -291,18 +300,19 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async revoke(membershipId: string): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_REVOKE); - this.setFunction(functions.MEMBERSHIP_REVOKE); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_REVOKE); + service.setFunction(functions.MEMBERSHIP_REVOKE); - this.arweaveTags = await this.getTxTags(); + service.arweaveTags = await service.getTxTags(); let data: { id: string, value: string }[]; - if (!this.isPublic) { - const memberships = await this.listAll(this.vaultId, { shouldDecrypt: false }); + if (!service.isPublic) { + const memberships = await this.listAll(service.vaultId, { shouldDecrypt: false }); const activeMembers = memberships.filter((member: Membership) => - member.id !== this.objectId + member.id !== service.objectId && (member.status === status.ACCEPTED || member.status === status.PENDING)); // rotate keys for all active members @@ -311,13 +321,13 @@ class MembershipService extends Service { const { publicKey } = await this.api.getUserPublicData(member.email); memberPublicKeys.set(member.id, publicKey); })); - const { memberKeys } = await this.rotateMemberKeys(memberPublicKeys); + const { memberKeys } = await service.rotateMemberKeys(memberPublicKeys); // upload new state for all active members data = []; await Promise.all(activeMembers.map(async (member: Membership) => { const memberService = new MembershipService(this.wallet, this.api); - memberService.setVaultId(this.vaultId); + memberService.setVaultId(service.vaultId); memberService.setObjectId(member.id); memberService.setObject(member); const dataTx = await memberService.mergeAndUploadState({ keys: memberKeys.get(member.id) }); @@ -326,11 +336,11 @@ class MembershipService extends Service { } const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - this.arweaveTags + service.vaultId, + { function: service.function, data }, + service.arweaveTags ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -340,16 +350,17 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async changeRole(membershipId: string, role: RoleType): Promise { - await this.setVaultContextFromMembershipId(membershipId); - this.setActionRef(actionRefs.MEMBERSHIP_CHANGE_ROLE); - this.setFunction(functions.MEMBERSHIP_CHANGE_ROLE); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + service.setActionRef(actionRefs.MEMBERSHIP_CHANGE_ROLE); + service.setFunction(functions.MEMBERSHIP_CHANGE_ROLE); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, role }, - await this.getTxTags() + service.vaultId, + { function: service.function, role }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -382,28 +393,28 @@ class MembershipService extends Service { * @returns Promise with corresponding transaction id */ public async inviteResend(membershipId: string): Promise { - const object = await this.api.getMembership(membershipId, this.vaultId); - this.setActionRef(actionRefs.MEMBERSHIP_INVITE_RESEND); - if (object.status !== status.PENDING && object.status !== status.INVITED) { + const membership = await this.api.getMembership(membershipId); + if (membership.status !== status.PENDING && membership.status !== status.INVITED) { throw new BadRequest("Cannot resend the invitation for member: " + membershipId + - ". Found invalid status: " + object.status); + ". Found invalid status: " + membership.status); } - await this.api.inviteResend(object.vaultId, membershipId); + await this.api.inviteResend(membership.vaultId, membershipId); } async profileUpdate(membershipId: string, name: string, avatar: ArrayBuffer): Promise { - await this.setVaultContextFromMembershipId(membershipId); - const memberDetails = await this.processMemberDetails({ name, avatar }, this.object.__cacheOnly__); - this.setActionRef(actionRefs.MEMBERSHIP_PROFILE_UPDATE); - this.setFunction(functions.MEMBERSHIP_UPDATE); + const service = new MembershipService(this.wallet, this.api); + await service.setVaultContextFromMembershipId(membershipId); + const memberDetails = await service.processMemberDetails({ name, avatar }, service.object.__cacheOnly__); + service.setActionRef(actionRefs.MEMBERSHIP_PROFILE_UPDATE); + service.setFunction(functions.MEMBERSHIP_UPDATE); - const data = await this.mergeAndUploadState({ memberDetails }); + const data = await service.mergeAndUploadState({ memberDetails }); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - await this.getTxTags() + service.vaultId, + { function: service.function, data }, + await service.getTxTags() ); - const membership = await this.processMembership(object, !this.isPublic, this.keys); + const membership = await this.processMembership(object, !service.isPublic, service.keys); return { transactionId: id, object: membership }; } @@ -450,6 +461,48 @@ class MembershipService extends Service { return null; } } + + async rotateMemberKeys(publicKeys: Map): Promise<{ + memberKeys: Map, + keyPair: Keys + }> { + const memberKeys = new Map(); + // generate a new vault key pair + const keyPair = await generateKeyPair(); + + for (let [memberId, publicKey] of publicKeys) { + const memberKeysEncrypter = new Encrypter( + this.wallet, + this.dataEncrypter.keys, + base64ToArray(publicKey) + ); + try { + memberKeys.set(memberId, [await memberKeysEncrypter.encryptMemberKey(keyPair)]); + } catch (error) { + throw new IncorrectEncryptionKey(error); + } + } + return { memberKeys, keyPair }; + } + + async processMemberDetails(memberDetails: { name?: string, avatar?: ArrayBuffer }, cacheOnly?: boolean) { + const processedMemberDetails = {} as ProfileDetails; + if (!this.isPublic) { + if (memberDetails.name) { + processedMemberDetails.name = await this.processWriteString(memberDetails.name); + } + if (memberDetails.avatar) { + const { resourceUrl, resourceTx } = await this.processAvatar(memberDetails.avatar, cacheOnly); + processedMemberDetails.avatarUri = [`arweave:${resourceTx}`, `s3:${resourceUrl}`]; + } + } + return new ProfileDetails(processedMemberDetails); + } + + private async processAvatar(avatar: ArrayBuffer, cacheOnly?: boolean): Promise<{ resourceTx: string, resourceUrl: string }> { + const { processedData, encryptionTags } = await this.processWriteRaw(avatar); + return this.api.uploadFile(processedData, encryptionTags, { cacheOnly, public: false }); + } }; export type MembershipCreateOptions = { diff --git a/src/core/memo.ts b/src/core/memo.ts index 48fdb74c..bee3b563 100644 --- a/src/core/memo.ts +++ b/src/core/memo.ts @@ -25,15 +25,16 @@ class MemoService extends NodeService { * @returns Promise with new node id & corresponding transaction id */ public async create(vaultId: string, message: string, options: NodeCreateOptions = this.defaultCreateOptions): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.MEMO_CREATE); - this.setFunction(functions.NODE_CREATE); - this.setAkordTags(options.tags); + const service = new MemoService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.MEMO_CREATE); + service.setFunction(functions.NODE_CREATE); + service.setAkordTags(options.tags); const state = { - versions: [await this.memoVersion(message)], + versions: [await service.memoVersion(message)], tags: options.tags || [] }; - const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); + const { nodeId, transactionId, object } = await service.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { memoId: nodeId, transactionId, object }; } @@ -43,22 +44,23 @@ class MemoService extends NodeService { * @returns Promise with corresponding transaction id */ public async addReaction(memoId: string, reaction: reactionEmoji): Promise { - await this.setVaultContextFromNodeId(memoId, this.objectType); - this.setActionRef(actionRefs.MEMO_ADD_REACTION); - this.setFunction(functions.NODE_UPDATE); - this.arweaveTags = await this.getTxTags(); + const service = new MemoService(this.wallet, this.api); + await service.setVaultContextFromNodeId(memoId, this.objectType); + service.setActionRef(actionRefs.MEMO_ADD_REACTION); + service.setFunction(functions.NODE_UPDATE); + service.arweaveTags = await service.getTxTags(); - const currentState = await this.getCurrentState(); + const currentState = await service.getCurrentState(); const newState = lodash.cloneDeepWith(currentState); - newState.versions[newState.versions.length - 1].reactions.push(await this.memoReaction(reaction)); - const dataTxId = await this.uploadState(newState); + newState.versions[newState.versions.length - 1].reactions.push(await service.memoReaction(reaction)); + const dataTxId = await service.uploadState(newState); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const memo = await this.processMemo(object, !this.isPublic, this.keys); + const memo = await this.processMemo(object, !service.isPublic, service.keys); return { transactionId: id, object: memo }; } @@ -68,20 +70,21 @@ class MemoService extends NodeService { * @returns Promise with corresponding transaction id */ public async removeReaction(memoId: string, reaction: reactionEmoji): Promise { - await this.setVaultContextFromNodeId(memoId, this.objectType); - this.setActionRef(actionRefs.MEMO_REMOVE_REACTION); - this.setFunction(functions.NODE_UPDATE); - this.arweaveTags = await this.getTxTags(); + const service = new MemoService(this.wallet, this.api); + await service.setVaultContextFromNodeId(memoId, this.objectType); + service.setActionRef(actionRefs.MEMO_REMOVE_REACTION); + service.setFunction(functions.NODE_UPDATE); + service.arweaveTags = await service.getTxTags(); - const state = await this.deleteReaction(reaction); - const dataTxId = await this.uploadState(state); + const state = await service.deleteReaction(reaction); + const dataTxId = await service.uploadState(state); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const memo = await this.processMemo(object, !this.isPublic, this.keys); + const memo = await this.processMemo(object, !service.isPublic, service.keys); return { transactionId: id, object: memo }; } diff --git a/src/core/node.ts b/src/core/node.ts index 58e2768e..44a304be 100644 --- a/src/core/node.ts +++ b/src/core/node.ts @@ -8,6 +8,7 @@ import { Paginated } from '../types/paginated'; import { v4 as uuidv4 } from "uuid"; import { IncorrectEncryptionKey } from '../errors/incorrect-encryption-key'; import { BadRequest } from '../errors/bad-request'; +import { handleListErrors, paginate } from './common'; class NodeService extends Service { objectType: NodeType; @@ -64,7 +65,7 @@ class NodeService extends Service { .map(async (nodeProto: any) => { return await this.processNode(nodeProto, !nodeProto.__public__ && listOptions.shouldDecrypt, nodeProto.__keys__); }); - const { items, errors } = await this.handleListErrors(response.items, promises); + const { items, errors } = await handleListErrors(response.items, promises); return { items, nextToken: response.nextToken, @@ -81,7 +82,7 @@ class NodeService extends Service { const list = async (options: ListOptions & { vaultId: string }) => { return await this.list(options.vaultId, options); } - return await this.paginate(list, { ...options, vaultId }); + return await paginate(list, { ...options, vaultId }); } /** @@ -90,13 +91,14 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async rename(nodeId: string, name: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType); - this.setActionRef(this.objectType.toUpperCase() + "_RENAME"); - this.setFunction(functions.NODE_UPDATE); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType); + service.setActionRef(this.objectType.toUpperCase() + "_RENAME"); + service.setFunction(functions.NODE_UPDATE); const state = { - name: await this.processWriteString(name) + name: await service.processWriteString(name) }; - return this.nodeUpdate(state); + return service.nodeUpdate(state) as Promise; } /** @@ -105,10 +107,11 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async move(nodeId: string, parentId?: string, vaultId?: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); - this.setActionRef(this.objectType.toUpperCase() + "_MOVE"); - this.setFunction(functions.NODE_MOVE); - return this.nodeUpdate(null, { parentId }); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); + service.setActionRef(this.objectType.toUpperCase() + "_MOVE"); + service.setFunction(functions.NODE_MOVE); + return service.nodeUpdate(null, { parentId }); } /** @@ -116,10 +119,11 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async revoke(nodeId: string, vaultId?: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); - this.setActionRef(this.objectType.toUpperCase() + "_REVOKE"); - this.setFunction(functions.NODE_REVOKE); - return this.nodeUpdate(); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); + service.setActionRef(this.objectType.toUpperCase() + "_REVOKE"); + service.setFunction(functions.NODE_REVOKE); + return service.nodeUpdate() as Promise; } /** @@ -127,10 +131,11 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async restore(nodeId: string, vaultId?: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); - this.setActionRef(this.objectType.toUpperCase() + "_RESTORE"); - this.setFunction(functions.NODE_RESTORE); - return this.nodeUpdate(); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); + service.setActionRef(this.objectType.toUpperCase() + "_RESTORE"); + service.setFunction(functions.NODE_RESTORE); + return service.nodeUpdate() as Promise; } /** @@ -138,10 +143,11 @@ class NodeService extends Service { * @returns Promise with corresponding transaction id */ public async delete(nodeId: string, vaultId?: string): Promise { - await this.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); - this.setActionRef(this.objectType.toUpperCase() + "_DELETE"); - this.setFunction(functions.NODE_DELETE); - return this.nodeUpdate(); + const service = new NodeService(this.wallet, this.api); + await service.setVaultContextFromNodeId(nodeId, this.objectType, vaultId); + service.setActionRef(this.objectType.toUpperCase() + "_DELETE"); + service.setFunction(functions.NODE_DELETE); + return service.nodeUpdate() as Promise; } protected async nodeCreate(state?: any, clientInput?: { parentId?: string }, clientTags?: Tags): Promise<{ diff --git a/src/core/profile.ts b/src/core/profile.ts index cc52a609..cd48a123 100644 --- a/src/core/profile.ts +++ b/src/core/profile.ts @@ -6,6 +6,9 @@ import { Service } from "./service"; import { objectType } from "../constants"; import { Membership } from "../types/membership"; import { ListOptions } from "../types/query-options"; +import { getEncryptedPayload, handleListErrors, paginate } from "./common"; +import { Encrypter, arrayToString } from "@akord/crypto"; +import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; class ProfileService extends Service { objectType = objectType.PROFILE; @@ -20,7 +23,42 @@ class ProfileService extends Service { shouldCacheDecider: () => CacheBusters.cache }) public async get(): Promise { - return await this.getProfileDetails(); + const user = await this.api.getUser(); + if (user) { + const profileEncrypter = new Encrypter(this.wallet, null, null); + profileEncrypter.decryptedKeys = [ + { + publicKey: this.wallet.publicKeyRaw(), + privateKey: this.wallet.privateKeyRaw() + } + ] + let avatar = null; + const resourceUri = getAvatarUri(new ProfileDetails(user)); + if (resourceUri) { + const { fileData, metadata } = await this.api.downloadFile(resourceUri); + const encryptedPayload = getEncryptedPayload(fileData, metadata); + try { + if (encryptedPayload) { + avatar = await profileEncrypter.decryptRaw(encryptedPayload, false); + } else { + const dataString = arrayToString(new Uint8Array(fileData)); + avatar = await profileEncrypter.decryptRaw(dataString, true); + } + } catch (error) { + throw new IncorrectEncryptionKey(error); + } + } + try { + const decryptedProfile = await profileEncrypter.decryptObject( + user, + ['name'], + ); + return { ...decryptedProfile, avatar } + } catch (error) { + throw new IncorrectEncryptionKey(error); + } + } + return {}; } /** @@ -38,11 +76,12 @@ class ProfileService extends Service { }> { // update profile const user = await this.api.getUser(); - this.setObject(user); - this.setRawDataEncryptionPublicKey(this.wallet.publicKeyRaw()); - this.setIsPublic(false); - const profileDetails = await this.processMemberDetails({ name, avatar }, true); + const service = new MembershipService(this.wallet, this.api); + + service.setRawDataEncryptionPublicKey(this.wallet.publicKeyRaw()); + service.setIsPublic(false); + const profileDetails = await service.processMemberDetails({ name, avatar }, true); const newProfileDetails = new ProfileDetails({ ...user, @@ -61,7 +100,7 @@ class ProfileService extends Service { transactions.push({ id: membership.id, transactionId: transactionId }); return membership; }) - const { errors } = await this.handleListErrors(memberships, membershipPromises); + const { errors } = await handleListErrors(memberships, membershipPromises); return { transactions, errors }; } @@ -69,10 +108,22 @@ class ProfileService extends Service { const list = async (listOptions: ListOptions) => { return await this.api.getMemberships(listOptions); } - return await this.paginate(list, {}); + return await paginate(list, {}); } }; +const getAvatarUri = (profileDetails: ProfileDetails) => { + if (profileDetails.avatarUri && profileDetails.avatarUri.length) { + const avatarUri = [...profileDetails.avatarUri] + .reverse() + .find(resourceUri => resourceUri.startsWith("s3:")) + ?.replace("s3:", ""); + return avatarUri !== "null" && avatarUri; + } else { + return null; + } +} + export { ProfileService } diff --git a/src/core/service.ts b/src/core/service.ts index 34bdddee..c17600e0 100644 --- a/src/core/service.ts +++ b/src/core/service.ts @@ -11,12 +11,9 @@ import { arrayToBase64, base64ToJson, deriveAddress, - EncryptedKeys, - generateKeyPair, - Keys + EncryptedKeys } from "@akord/crypto"; import { objectType, protocolTags, functions, dataTags, encryptionTags, smartweaveTags, AKORD_TAG } from '../constants'; -import lodash from "lodash"; import { Vault } from "../types/vault"; import { Tag, Tags } from "../types/contract"; import { NodeLike } from "../types/node"; @@ -24,8 +21,7 @@ import { Membership } from "../types/membership"; import { Object, ObjectType } from "../types/object"; import { EncryptedPayload } from "@akord/crypto/lib/types"; import { IncorrectEncryptionKey } from "../errors/incorrect-encryption-key"; -import { ProfileDetails } from "../types/profile-details"; -import { ListOptions } from "../types/query-options"; +import { getEncryptedPayload, mergeState } from "./common"; export type EncryptionMetadata = { encryptedKey?: string, @@ -128,21 +124,18 @@ class Service { this.tags = tags; } - async processReadRaw(data: ArrayBuffer | string, metadata: EncryptionMetadata, shouldDecrypt = true): Promise { - if (this.isPublic || !shouldDecrypt) { - return data as ArrayBuffer; - } - - const encryptedPayload = this.getEncryptedPayload(data, metadata); + async processWriteString(data: string): Promise { + if (this.isPublic) return data; + let encryptedPayload: string; try { - if (encryptedPayload) { - return this.dataEncrypter.decryptRaw(encryptedPayload, false); - } else { - return this.dataEncrypter.decryptRaw(data as string); - } + encryptedPayload = await this.dataEncrypter.encryptRaw(stringToArray(data)) as string; } catch (error) { throw new IncorrectEncryptionKey(error); } + const decodedPayload = base64ToJson(encryptedPayload) as any; + decodedPayload.publicAddress = (await this.getActiveKey()).address; + delete decodedPayload.publicKey; + return jsonToBase64(decodedPayload); } async setVaultContext(vaultId: string) { @@ -176,43 +169,48 @@ class Service { } } - protected async getProfileDetails(): Promise { - const user = await this.api.getUser(); - if (user) { - const profileEncrypter = new Encrypter(this.wallet, null, null); - profileEncrypter.decryptedKeys = [ - { - publicKey: this.wallet.publicKeyRaw(), - privateKey: this.wallet.privateKeyRaw() - } - ] - let avatar = null; - const resourceUri = this.getAvatarUri(new ProfileDetails(user)); - if (resourceUri) { - const { fileData, metadata } = await this.api.downloadFile(resourceUri); - const encryptedPayload = this.getEncryptedPayload(fileData, metadata); - try { - if (encryptedPayload) { - avatar = await profileEncrypter.decryptRaw(encryptedPayload, false); - } else { - const dataString = arrayToString(new Uint8Array(fileData)); - avatar = await profileEncrypter.decryptRaw(dataString, true); - } - } catch (error) { - throw new IncorrectEncryptionKey(error); - } - } - try { - const decryptedProfile = await profileEncrypter.decryptObject( - user, - ['name'], - ); - return { ...decryptedProfile, avatar } - } catch (error) { - throw new IncorrectEncryptionKey(error); - } + async uploadState(state: any, cacheOnly = false): Promise { + const signature = await this.signData(state); + const tags = [ + new Tag(dataTags.DATA_TYPE, "State"), + new Tag(smartweaveTags.CONTENT_TYPE, STATE_CONTENT_TYPE), + new Tag(protocolTags.SIGNATURE, signature), + new Tag(protocolTags.SIGNER_ADDRESS, await this.wallet.getAddress()), + new Tag(protocolTags.VAULT_ID, this.vaultId), + new Tag(protocolTags.NODE_TYPE, this.objectType), + ] + if (this.objectType === objectType.MEMBERSHIP) { + tags.push(new Tag(protocolTags.MEMBERSHIP_ID, this.objectId)) + } else if (this.objectType !== objectType.VAULT) { + tags.push(new Tag(protocolTags.NODE_ID, this.objectId)) } - return {}; + const ids = await this.api.uploadData([{ data: state, tags }], { cacheOnly }); + return ids[0]; + } + + async getTxTags(): Promise { + const tags = [ + new Tag(protocolTags.FUNCTION_NAME, this.function), + new Tag(protocolTags.SIGNER_ADDRESS, await this.wallet.getAddress()), + new Tag(protocolTags.VAULT_ID, this.vaultId), + new Tag(protocolTags.TIMESTAMP, JSON.stringify(Date.now())), + new Tag(protocolTags.NODE_TYPE, this.objectType), + new Tag(protocolTags.PUBLIC, this.isPublic ? "true" : "false"), + ] + if (this.groupRef) { + tags.push(new Tag(protocolTags.GROUP_REF, this.groupRef)); + } + if (this.actionRef) { + tags.push(new Tag(protocolTags.ACTION_REF, this.actionRef)); + } + this.tags + ?.filter(tag => tag) + ?.map((tag: string) => + tag?.split(" ").join(",").split(".").join(",").split(",").map((value: string) => + tags.push(new Tag(AKORD_TAG, value.toLowerCase()))) + ); + // remove duplicates + return [...new Map(tags.map(item => [item.value, item])).values()]; } protected async processWriteRaw(data: ArrayBuffer, encryptedKey?: string) { @@ -236,53 +234,21 @@ class Service { return { processedData, encryptionTags: tags } } - protected async getActiveKey() { - return { - address: await deriveAddress(this.dataEncrypter.publicKey), - publicKey: arrayToBase64(this.dataEncrypter.publicKey) - }; - } + protected async processReadRaw(data: ArrayBuffer | string, metadata: EncryptionMetadata, shouldDecrypt = true): Promise { + if (this.isPublic || !shouldDecrypt) { + return data as ArrayBuffer; + } - async processWriteString(data: string): Promise { - if (this.isPublic) return data; - let encryptedPayload: string; + const encryptedPayload = getEncryptedPayload(data, metadata); try { - encryptedPayload = await this.dataEncrypter.encryptRaw(stringToArray(data)) as string; + if (encryptedPayload) { + return this.dataEncrypter.decryptRaw(encryptedPayload, false); + } else { + return this.dataEncrypter.decryptRaw(data as string); + } } catch (error) { throw new IncorrectEncryptionKey(error); } - const decodedPayload = base64ToJson(encryptedPayload) as any; - decodedPayload.publicAddress = (await this.getActiveKey()).address; - delete decodedPayload.publicKey; - return jsonToBase64(decodedPayload); - } - - protected getAvatarUri(profileDetails: ProfileDetails) { - if (profileDetails.avatarUri && profileDetails.avatarUri.length) { - const avatarUri = [...profileDetails.avatarUri].reverse().find(resourceUri => resourceUri.startsWith("s3:"))?.replace("s3:", ""); - return avatarUri !== "null" && avatarUri; - } else { - return null; - } - } - - protected async processAvatar(avatar: ArrayBuffer, cacheOnly?: boolean): Promise<{ resourceTx: string, resourceUrl: string }> { - const { processedData, encryptionTags } = await this.processWriteRaw(avatar); - return this.api.uploadFile(processedData, encryptionTags, { cacheOnly, public: false }); - } - - protected async processMemberDetails(memberDetails: { name?: string, avatar?: ArrayBuffer }, cacheOnly?: boolean) { - const processedMemberDetails = {} as ProfileDetails; - if (!this.isPublic) { - if (memberDetails.name) { - processedMemberDetails.name = await this.processWriteString(memberDetails.name); - } - if (memberDetails.avatar) { - const { resourceUrl, resourceTx } = await this.processAvatar(memberDetails.avatar, cacheOnly); - processedMemberDetails.avatarUri = [`arweave:${resourceTx}`, `s3:${resourceUrl}`]; - } - } - return new ProfileDetails(processedMemberDetails); } protected async processReadString(data: string, shouldDecrypt = true): Promise { @@ -291,18 +257,11 @@ class Service { return arrayToString(decryptedDataRaw); } - protected getEncryptedPayload(data: ArrayBuffer | string, metadata: EncryptionMetadata): EncryptedPayload { - const { encryptedKey, iv } = metadata; - if (encryptedKey && iv) { - return { - encryptedKey, - encryptedData: { - iv: base64ToArray(iv), - ciphertext: data as ArrayBuffer - } - } - } - return null; + protected async getActiveKey() { + return { + address: await deriveAddress(this.dataEncrypter.publicKey), + publicKey: arrayToBase64(this.dataEncrypter.publicKey) + }; } protected async getCurrentState(): Promise { @@ -313,11 +272,11 @@ class Service { protected async mergeAndUploadState(stateUpdates: any): Promise { const currentState = await this.getCurrentState(); - const mergedState = await this.mergeState(currentState, stateUpdates); + const mergedState = mergeState(currentState, stateUpdates); return this.uploadState(mergedState); } - protected async signData(data: any): Promise { + private async signData(data: any): Promise { const privateKeyRaw = this.wallet.signingPrivateKeyRaw(); const signature = await signString( jsonToBase64(data), @@ -325,112 +284,6 @@ class Service { ); return signature; } - - public async uploadState(state: any, cacheOnly = false): Promise { - const signature = await this.signData(state); - const tags = [ - new Tag(dataTags.DATA_TYPE, "State"), - new Tag(smartweaveTags.CONTENT_TYPE, STATE_CONTENT_TYPE), - new Tag(protocolTags.SIGNATURE, signature), - new Tag(protocolTags.SIGNER_ADDRESS, await this.wallet.getAddress()), - new Tag(protocolTags.VAULT_ID, this.vaultId), - new Tag(protocolTags.NODE_TYPE, this.objectType), - ] - if (this.objectType === objectType.MEMBERSHIP) { - tags.push(new Tag(protocolTags.MEMBERSHIP_ID, this.objectId)) - } else if (this.objectType !== objectType.VAULT) { - tags.push(new Tag(protocolTags.NODE_ID, this.objectId)) - } - const ids = await this.api.uploadData([{ data: state, tags }], { cacheOnly }); - return ids[0]; - } - - protected async mergeState(currentState: any, stateUpdates: any) { - let newState = lodash.cloneDeepWith(currentState); - lodash.mergeWith( - newState, - stateUpdates, - function concatArrays(objValue, srcValue) { - if (lodash.isArray(objValue)) { - return objValue.concat(srcValue); - } - }); - return newState; - } - - async getTxTags(): Promise { - const tags = [ - new Tag(protocolTags.FUNCTION_NAME, this.function), - new Tag(protocolTags.SIGNER_ADDRESS, await this.wallet.getAddress()), - new Tag(protocolTags.VAULT_ID, this.vaultId), - new Tag(protocolTags.TIMESTAMP, JSON.stringify(Date.now())), - new Tag(protocolTags.NODE_TYPE, this.objectType), - new Tag(protocolTags.PUBLIC, this.isPublic ? "true" : "false"), - ] - if (this.groupRef) { - tags.push(new Tag(protocolTags.GROUP_REF, this.groupRef)); - } - if (this.actionRef) { - tags.push(new Tag(protocolTags.ACTION_REF, this.actionRef)); - } - this.tags - ?.filter(tag => tag) - ?.map((tag: string) => - tag?.split(" ").join(",").split(".").join(",").split(",").map((value: string) => - tags.push(new Tag(AKORD_TAG, value.toLowerCase()))) - ); - // remove duplicates - return [...new Map(tags.map(item => [item.value, item])).values()]; - } - - protected async handleListErrors(originalItems: Array, promises: Array>) - : Promise<{ items: Array, errors: Array<{ id: string, error: Error }> }> { - const results = await Promise.all(promises.map(p => p.catch(e => e))); - const items = results.filter(result => !(result instanceof Error)); - const errors = results - .map((result, index) => ({ result, index })) - .filter((mapped) => mapped.result instanceof Error) - .map((filtered) => ({ id: (originalItems[filtered.index]).id, error: filtered.result })); - return { items, errors }; - } - - protected async paginate(apiCall: any, listOptions: ListOptions & { vaultId?: string }): Promise> { - let token = undefined; - let results = [] as T[]; - do { - const { items, nextToken } = await apiCall(listOptions); - results = results.concat(items); - token = nextToken; - listOptions.nextToken = nextToken; - if (nextToken === "null") { - token = undefined; - } - } while (token); - return results; - } - - protected async rotateMemberKeys(publicKeys: Map): Promise<{ - memberKeys: Map, - keyPair: Keys - }> { - const memberKeys = new Map(); - // generate a new vault key pair - const keyPair = await generateKeyPair(); - - for (let [memberId, publicKey] of publicKeys) { - const memberKeysEncrypter = new Encrypter( - this.wallet, - this.dataEncrypter.keys, - base64ToArray(publicKey) - ); - try { - memberKeys.set(memberId, [await memberKeysEncrypter.encryptMemberKey(keyPair)]); - } catch (error) { - throw new IncorrectEncryptionKey(error); - } - } - return { memberKeys, keyPair }; - } } export { diff --git a/src/core/stack.ts b/src/core/stack.ts index 0effd0c9..b425ed76 100644 --- a/src/core/stack.ts +++ b/src/core/stack.ts @@ -1,6 +1,6 @@ import { NodeCreateOptions, NodeService } from "./node"; import { actionRefs, functions, objectType } from "../constants"; -import { FileService, FileUploadResult, FileUploadOptions } from "./file"; +import { FileService, FileUploadOptions } from "./file"; import { FileLike } from "../types/file"; import { FileVersion, Stack, StorageType, nodeType } from "../types/node"; @@ -22,20 +22,24 @@ class StackService extends NodeService { ...this.defaultCreateOptions, ...options } - await this.setVaultContext(vaultId); - this.setVaultContextForFile(); - this.setActionRef(actionRefs.STACK_CREATE); - this.setFunction(functions.NODE_CREATE); - this.setAkordTags((this.isPublic ? [name] : []).concat(createOptions.tags)); + const service = new StackService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.STACK_CREATE); + service.setFunction(functions.NODE_CREATE); + service.setAkordTags((service.isPublic ? [name] : []).concat(createOptions.tags)); - createOptions.cacheOnly = this.vault.cacheOnly; + createOptions.cacheOnly = service.vault.cacheOnly; + + const fileService = new FileService(this.wallet, this.api, service); + const fileUploadResult = await fileService.create(file, createOptions); + const version = await fileService.newVersion(file, fileUploadResult); const state = { - name: await this.processWriteString(name ? name : file.name), - versions: [await this.uploadNewFileVersion(file, createOptions)], + name: await service.processWriteString(name ? name : file.name), + versions: [version], tags: createOptions.tags || [] }; - const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: createOptions.parentId }, options.arweaveTags); + const { nodeId, transactionId, object } = await service.nodeCreate(state, { parentId: createOptions.parentId }, options.arweaveTags); return { stackId: nodeId, transactionId, object }; } @@ -46,25 +50,26 @@ class StackService extends NodeService { * @returns Promise with new stack id & corresponding transaction id */ public async import(vaultId: string, fileTxId: string, options: NodeCreateOptions = this.defaultCreateOptions): Promise { - await this.setVaultContext(vaultId); - this.setVaultContextForFile(); - this.setActionRef(actionRefs.STACK_CREATE); - this.setFunction(functions.NODE_CREATE); + const service = new StackService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.STACK_CREATE); + service.setFunction(functions.NODE_CREATE); - const { file, resourceHash, resourceUrl } = await this.fileService.import(fileTxId); + const fileService = new FileService(this.wallet, this.api, service); + const { file, resourceHash, resourceUrl } = await fileService.import(fileTxId); const version = new FileVersion({ owner: await this.wallet.getAddress(), createdAt: JSON.stringify(Date.now()), - name: await this.processWriteString(file.name), + name: await service.processWriteString(file.name), type: file.type, size: file.size, resourceUri: [`arweave:${fileTxId}`, `hash:${resourceHash}`, `s3:${resourceUrl}`], }); const state = { - name: await this.processWriteString(file.name), + name: await service.processWriteString(file.name), versions: [version] }; - const { nodeId, transactionId, object } = await this.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); + const { nodeId, transactionId, object } = await service.nodeCreate(state, { parentId: options.parentId }, options.arweaveTags); return { stackId: nodeId, transactionId, object }; } @@ -75,17 +80,21 @@ class StackService extends NodeService { * @returns Promise with corresponding transaction id */ public async uploadRevision(stackId: string, file: FileLike, options: FileUploadOptions = {}): Promise { - await this.setVaultContextFromNodeId(stackId, this.objectType); - this.setVaultContextForFile(); - this.setActionRef(actionRefs.STACK_UPLOAD_REVISION); + const service = new StackService(this.wallet, this.api); + await service.setVaultContextFromNodeId(stackId, this.objectType); + service.setActionRef(actionRefs.STACK_UPLOAD_REVISION); + service.setFunction(functions.NODE_UPDATE); + + options.cacheOnly = service.object.__cacheOnly__; - options.cacheOnly = this.object.__cacheOnly__; + const fileService = new FileService(this.wallet, this.api, service); + const fileUploadResult = await fileService.create(file, options); + const version = await fileService.newVersion(file, fileUploadResult); const state = { - versions: [await this.uploadNewFileVersion(file, options)] + versions: [version] }; - this.setFunction(functions.NODE_UPDATE); - return this.nodeUpdate(state); + return service.nodeUpdate(state); } /** @@ -95,13 +104,13 @@ class StackService extends NodeService { * @returns Promise with version name & data buffer */ public async getVersion(stackId: string, index?: number): Promise<{ name: string, data: ArrayBuffer }> { - const stack = new Stack(await this.api.getNode(stackId, objectType.STACK, this.vaultId), null); + const stack = new Stack(await this.api.getNode(stackId, objectType.STACK), null); const version = stack.getVersion(index); - await this.setVaultContext(stack.vaultId); - this.setVaultContextForFile(); - const { fileData, metadata } = await this.api.downloadFile(version.getUri(StorageType.S3), { public: this.isPublic }); - const data = await this.processReadRaw(fileData, metadata); - const name = await this.processReadString(version.name); + const service = new StackService(this.wallet, this.api); + await service.setVaultContext(stack.vaultId); + const { fileData, metadata } = await this.api.downloadFile(version.getUri(StorageType.S3), { public: service.isPublic }); + const data = await service.processReadRaw(fileData, metadata); + const name = await service.processReadString(version.name); return { name, data }; } @@ -113,37 +122,9 @@ class StackService extends NodeService { * @returns Promise with stack file uri */ public async getUri(stackId: string, type: StorageType = StorageType.ARWEAVE, index?: number): Promise { - const stack = new Stack(await this.api.getNode(stackId, objectType.STACK, this.vaultId), null); + const stack = new Stack(await this.api.getNode(stackId, objectType.STACK), null); return stack.getUri(type, index); } - - public async uploadNewFileVersion(file: FileLike, options: FileUploadOptions): Promise { - const { - resourceTx, - resourceUrl, - resourceHash, - numberOfChunks, - chunkSize - } = await this.fileService.create(file, options); - const version = new FileVersion({ - owner: await this.wallet.getAddress(), - createdAt: JSON.stringify(Date.now()), - name: await this.processWriteString(file.name), - type: file.type, - size: file.size, - resourceUri: [`arweave:${resourceTx}`, `hash:${resourceHash}`, `s3:${resourceUrl}`], - numberOfChunks, - chunkSize, - }); - return version; - } - - setVaultContextForFile(): void { - this.fileService.setKeys(this.keys); - this.fileService.setRawDataEncryptionPublicKey(this.dataEncrypter.publicKey); - this.fileService.setVaultId(this.vaultId); - this.fileService.setIsPublic(this.isPublic); - } }; export type StackCreateOptions = NodeCreateOptions & FileUploadOptions; diff --git a/src/core/vault.ts b/src/core/vault.ts index 094889f9..af25ba14 100644 --- a/src/core/vault.ts +++ b/src/core/vault.ts @@ -11,6 +11,8 @@ import { MembershipService } from "./membership"; import lodash from "lodash"; import { NotFound } from "../errors/not-found"; import { BadRequest } from "../errors/bad-request"; +import { handleListErrors, paginate } from "./common"; +import { ProfileService } from "./profile"; class VaultService extends Service { objectType = objectType.VAULT; @@ -61,7 +63,7 @@ class VaultService extends Service { .map(async (vaultProto: Vault) => { return await this.processVault(vaultProto, listOptions.shouldDecrypt, vaultProto.keys); }) as Promise[]; - const { items, errors } = await this.handleListErrors(response.items, promises); + const { items, errors } = await handleListErrors(response.items, promises); return { items, nextToken: response.nextToken, @@ -77,7 +79,7 @@ class VaultService extends Service { const list = async (listOptions: ListOptions) => { return await this.list(listOptions); } - return await this.paginate(list, options); + return await paginate(list, options); } /** @@ -98,58 +100,67 @@ class VaultService extends Service { vaultId = await this.api.initContractId([new Tag(protocolTags.NODE_TYPE, objectType.VAULT)]); } - const memberDetails = await this.getProfileDetails(); - this.setActionRef(actionRefs.VAULT_CREATE); - this.setIsPublic(createOptions.public); - this.setFunction(functions.VAULT_CREATE); - this.setVaultId(vaultId); - this.setObjectId(vaultId); - this.setAkordTags((this.isPublic ? [name] : []).concat(createOptions.tags)); + const profileService = new ProfileService(this.wallet, this.api); + const memberDetails = await profileService.get(); + + const service = new VaultService(this.wallet, this.api); + service.setActionRef(actionRefs.VAULT_CREATE); + service.setIsPublic(createOptions.public); + service.setFunction(functions.VAULT_CREATE); + service.setVaultId(vaultId); + service.setObjectId(vaultId); + service.setAkordTags((service.isPublic ? [name] : []).concat(createOptions.tags)); const address = await this.wallet.getAddress(); const membershipId = uuidv4(); - this.arweaveTags = [ + service.arweaveTags = [ new Tag(protocolTags.MEMBER_ADDRESS, address), new Tag(protocolTags.MEMBERSHIP_ID, membershipId), - ].concat(await this.getTxTags()); - createOptions.arweaveTags?.map((tag: Tag) => this.arweaveTags.push(tag)); + ].concat(await service.getTxTags()); + createOptions.arweaveTags?.map((tag: Tag) => service.arweaveTags.push(tag)); + + const memberService = new MembershipService(this.wallet, this.api, service); + memberService.setVaultId(service.vaultId); + memberService.setObjectId(membershipId); let keys: EncryptedKeys[]; - if (!this.isPublic) { - const { memberKeys, keyPair } = await this.rotateMemberKeys(new Map([[membershipId, this.wallet.publicKey()]])); + if (!service.isPublic) { + const { memberKeys, keyPair } = await memberService.rotateMemberKeys( + new Map([[membershipId, this.wallet.publicKey()]]) + ); keys = memberKeys.get(membershipId); - this.setRawDataEncryptionPublicKey(keyPair.publicKey); - this.setKeys([{ encPublicKey: keys[0].encPublicKey, encPrivateKey: keys[0].encPrivateKey }]); + service.setRawDataEncryptionPublicKey(keyPair.publicKey); + service.setKeys([{ encPublicKey: keys[0].encPublicKey, encPrivateKey: keys[0].encPrivateKey }]); + memberService.setRawDataEncryptionPublicKey(keyPair.publicKey); + memberService.setKeys([{ encPublicKey: keys[0].encPublicKey, encPrivateKey: keys[0].encPrivateKey }]); } const vaultState = { - name: await this.processWriteString(name), + name: await service.processWriteString(name), termsOfAccess: createOptions.termsOfAccess, - description: createOptions.description ? await this.processWriteString(createOptions.description) : undefined, + description: createOptions.description ? await service.processWriteString(createOptions.description) : undefined, tags: createOptions.tags || [] } - const vaultStateTx = await this.uploadState(vaultState, createOptions.cacheOnly); + const vaultStateTx = await service.uploadState(vaultState, createOptions.cacheOnly); const memberState = { keys, - encPublicSigningKey: await this.processWriteString(this.wallet.signingPublicKey()), - memberDetails: await this.processMemberDetails(memberDetails, createOptions.cacheOnly) + encPublicSigningKey: await memberService.processWriteString(this.wallet.signingPublicKey()), + memberDetails: await memberService.processMemberDetails(memberDetails, createOptions.cacheOnly) } - const memberService = new MembershipService(this.wallet, this.api); - memberService.setVaultId(this.vaultId); - memberService.setObjectId(membershipId); + const memberStateTx = await memberService.uploadState(memberState, createOptions.cacheOnly); const data = { vault: vaultStateTx, membership: memberStateTx }; const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - this.arweaveTags, + service.vaultId, + { function: service.function, data }, + service.arweaveTags, { cacheOnly: createOptions.cacheOnly } ); - const vault = await this.processVault(object, true, this.keys); + const vault = await service.processVault(object, true, service.keys); return { vaultId, membershipId, transactionId: id, object: vault }; } @@ -162,32 +173,33 @@ class VaultService extends Service { if (!options.name && !options.tags && !options.description) { throw new BadRequest("Nothing to update"); } - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_UPDATE_METADATA); - this.setFunction(functions.VAULT_UPDATE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_UPDATE_METADATA); + service.setFunction(functions.VAULT_UPDATE); - const currentState = await this.getCurrentState(); + const currentState = await service.getCurrentState(); const newState = lodash.cloneDeepWith(currentState); if (options.name) { - newState.name = await this.processWriteString(options.name); + newState.name = await service.processWriteString(options.name); } if (options.tags) { newState.tags = options.tags; } if (options.description) { - newState.description = await this.processWriteString(options.description); + newState.description = await service.processWriteString(options.description); } - this.setAkordTags((options.name && this.isPublic ? [options.name] : []).concat(options.tags)); - this.arweaveTags = await this.getTxTags(); + service.setAkordTags((options.name && service.isPublic ? [options.name] : []).concat(options.tags)); + service.arweaveTags = await service.getTxTags(); - const dataTxId = await this.uploadState(newState); + const dataTxId = await service.uploadState(newState); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -197,22 +209,23 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async rename(vaultId: string, name: string): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_RENAME); - this.setFunction(functions.VAULT_UPDATE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_RENAME); + service.setFunction(functions.VAULT_UPDATE); const state = { - name: await this.processWriteString(name) + name: await service.processWriteString(name) }; - const data = await this.mergeAndUploadState(state); - this.setAkordTags(this.isPublic ? [name] : []); - this.arweaveTags = await this.getTxTags(); + const data = await service.mergeAndUploadState(state); + service.setAkordTags(service.isPublic ? [name] : []); + service.arweaveTags = await service.getTxTags(); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data }, - this.arweaveTags + service.vaultId, + { function: service.function, data }, + service.arweaveTags ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -222,14 +235,15 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async addTags(vaultId: string, tags: string[]): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_ADD_TAGS); - this.setFunction(functions.VAULT_UPDATE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_ADD_TAGS); + service.setFunction(functions.VAULT_UPDATE); - this.setAkordTags(tags); - this.arweaveTags = await this.getTxTags(); + service.setAkordTags(tags); + service.arweaveTags = await service.getTxTags(); - const currentState = await this.getCurrentState(); + const currentState = await service.getCurrentState(); const newState = lodash.cloneDeepWith(currentState); if (!newState.tags) { newState.tags = []; @@ -239,14 +253,14 @@ class VaultService extends Service { newState.tags.push(tag); } } - const dataTxId = await this.uploadState(newState); + const dataTxId = await service.uploadState(newState); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -256,12 +270,13 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async removeTags(vaultId: string, tags: string[]): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_REMOVE_TAGS); - this.setFunction(functions.VAULT_UPDATE); - this.arweaveTags = await this.getTxTags(); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_REMOVE_TAGS); + service.setFunction(functions.VAULT_UPDATE); + service.arweaveTags = await service.getTxTags(); - const currentState = await this.getCurrentState(); + const currentState = await service.getCurrentState(); const newState = lodash.cloneDeepWith(currentState); if (!newState.tags || newState.tags.length === 0) { throw new BadRequest("Tags cannot be removed, vault does not have any"); @@ -270,14 +285,14 @@ class VaultService extends Service { const index = this.getTagIndex(newState.tags, tag); newState.tags.splice(index, 1); } - const dataTxId = await this.uploadState(newState); + const dataTxId = await service.uploadState(newState); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function, data: dataTxId }, - this.arweaveTags + service.vaultId, + { function: service.function, data: dataTxId }, + service.arweaveTags ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -286,16 +301,17 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async archive(vaultId: string): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_ARCHIVE); - this.setFunction(functions.VAULT_ARCHIVE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_ARCHIVE); + service.setFunction(functions.VAULT_ARCHIVE); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function }, - await this.getTxTags() + service.vaultId, + { function: service.function }, + await service.getTxTags() ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; } @@ -304,16 +320,17 @@ class VaultService extends Service { * @returns Promise with corresponding transaction id */ public async restore(vaultId: string): Promise { - await this.setVaultContext(vaultId); - this.setActionRef(actionRefs.VAULT_RESTORE); - this.setFunction(functions.VAULT_RESTORE); + const service = new VaultService(this.wallet, this.api); + await service.setVaultContext(vaultId); + service.setActionRef(actionRefs.VAULT_RESTORE); + service.setFunction(functions.VAULT_RESTORE); const { id, object } = await this.api.postContractTransaction( - this.vaultId, - { function: this.function }, - await this.getTxTags() + service.vaultId, + { function: service.function }, + await service.getTxTags() ); - const vault = await this.processVault(object, true, this.keys); + const vault = await this.processVault(object, true, service.keys); return { transactionId: id, object: vault }; }