diff --git a/package.json b/package.json index 416f7ba5..703ddf8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ardrive-cli", - "version": "1.0.3", + "version": "1.0.4", "description": "The ArDrive Command Line Interface (CLI is a Node.js application for terminal-based ArDrive workflows. It also offers utility operations for securely interacting with Arweave wallets and inspecting various Arweave blockchain conditions.", "main": "./lib/index.js", "bin": { diff --git a/src/ardrive.ts b/src/ardrive.ts index 2f0989af..92d63c0c 100644 --- a/src/ardrive.ts +++ b/src/ardrive.ts @@ -521,7 +521,7 @@ export class ArDrive extends ArDriveAnonymous { }: UploadPublicFileParams): Promise { const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId); - const owner = await this.getOwnerForDriveId(driveId); + const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId); await this.assertOwnerAddress(owner); // Derive destination name and names already within provided destination folder @@ -610,7 +610,7 @@ export class ArDrive extends ArDriveAnonymous { }: BulkPublicUploadParams): Promise { const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId); - const owner = await this.getOwnerForDriveId(driveId); + const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId); await this.assertOwnerAddress(owner); // Derive destination name and names already within provided destination folder @@ -817,7 +817,7 @@ export class ArDrive extends ArDriveAnonymous { }: UploadPrivateFileParams): Promise { const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId); - const owner = await this.getOwnerForDriveId(driveId); + const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId, driveKey); await this.assertOwnerAddress(owner); // Derive destination name and names already within provided destination folder @@ -920,7 +920,7 @@ export class ArDrive extends ArDriveAnonymous { const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId); // Get owner of drive, will error if no drives are found - const owner = await this.getOwnerForDriveId(driveId); + const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId, driveKey); // Assert that the provided wallet is the owner of the drive await this.assertOwnerAddress(owner); @@ -1134,7 +1134,7 @@ export class ArDrive extends ArDriveAnonymous { } async createPublicFolder({ folderName, driveId, parentFolderId }: CreatePublicFolderParams): Promise { - const owner = await this.getOwnerForDriveId(driveId); + const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId); await this.assertOwnerAddress(owner); // Assert that there are no duplicate names in the destination folder @@ -1179,7 +1179,7 @@ export class ArDrive extends ArDriveAnonymous { driveKey, parentFolderId }: CreatePrivateFolderParams): Promise { - const owner = await this.getOwnerForDriveId(driveId); + const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId, driveKey); await this.assertOwnerAddress(owner); // Assert that there are no duplicate names in the destination folder diff --git a/src/arfsdao.ts b/src/arfsdao.ts index 253ed079..e67d1663 100644 --- a/src/arfsdao.ts +++ b/src/arfsdao.ts @@ -3,7 +3,15 @@ import type { JWKWallet, Wallet } from './wallet'; import Arweave from 'arweave'; import { v4 as uuidv4 } from 'uuid'; import Transaction from 'arweave/node/lib/transaction'; -import { deriveDriveKey, GQLEdgeInterface, GQLNodeInterface, GQLTagInterface, JWKInterface } from 'ardrive-core-js'; +import { + deriveDriveKey, + GQLEdgeInterface, + GQLNodeInterface, + GQLTagInterface, + JWKInterface, + DrivePrivacy, + driveDecrypt +} from 'ardrive-core-js'; import { ArFSPublicFileDataPrototype, ArFSObjectMetadataPrototype, @@ -908,6 +916,57 @@ export class ArFSDAO extends ArFSDAOAnonymous { ); } + public async getOwnerAndAssertDrive(driveId: DriveID, driveKey?: DriveKey): Promise { + const gqlQuery = buildQuery({ + tags: [ + { name: 'Entity-Type', value: 'drive' }, + { name: 'Drive-Id', value: `${driveId}` } + ], + sort: ASCENDING_ORDER + }); + const response = await this.arweave.api.post(graphQLURL, gqlQuery); + const edges: GQLEdgeInterface[] = response.data.data.transactions.edges; + + if (!edges.length) { + throw new Error(`Could not find a transaction with "Drive-Id": ${driveId}`); + } + + const edgeOfFirstDrive = edges[0]; + + const drivePrivacy: DrivePrivacy = driveKey ? 'private' : 'public'; + const drivePrivacyFromTag = edgeOfFirstDrive.node.tags.find((t) => t.name === 'Drive-Privacy'); + + if (!drivePrivacyFromTag) { + throw new Error('Target drive has no "Drive-Privacy" tag!'); + } + + if (drivePrivacyFromTag.value !== drivePrivacy) { + throw new Error(`Target drive is not a ${drivePrivacy} drive!`); + } + + if (driveKey) { + const cipherIVFromTag = edgeOfFirstDrive.node.tags.find((t) => t.name === 'Cipher-IV'); + if (!cipherIVFromTag) { + throw new Error('Target private drive has no "Cipher-IV" tag!'); + } + + const driveDataBuffer = Buffer.from( + await this.arweave.transactions.getData(edgeOfFirstDrive.node.id, { decode: true }) + ); + + try { + // Attempt to decrypt drive to assert drive key is correct + await driveDecrypt(cipherIVFromTag.value, driveKey, driveDataBuffer); + } catch { + throw new Error('Provided drive key or password could not decrypt target private drive!'); + } + } + + const driveOwnerAddress = edgeOfFirstDrive.node.owner.address; + + return new ArweaveAddress(driveOwnerAddress); + } + /** * Lists the children of certain private folder * @param {FolderID} folderId the folder ID to list children of diff --git a/src/arfsdao_anonymous.ts b/src/arfsdao_anonymous.ts index c4f52e18..66d4645e 100644 --- a/src/arfsdao_anonymous.ts +++ b/src/arfsdao_anonymous.ts @@ -46,7 +46,13 @@ export class ArFSDAOAnonymous extends ArFSDAOType { } public async getOwnerForDriveId(driveId: DriveID): Promise { - const gqlQuery = buildQuery({ tags: [{ name: 'Drive-Id', value: driveId }], sort: ASCENDING_ORDER }); + const gqlQuery = buildQuery({ + tags: [ + { name: 'Drive-Id', value: `${driveId}` }, + { name: 'Entity-Type', value: 'drive' } + ], + sort: ASCENDING_ORDER + }); const response = await this.arweave.api.post(graphQLURL, gqlQuery); const edges: GQLEdgeInterface[] = response.data.data.transactions.edges; @@ -55,6 +61,7 @@ export class ArFSDAOAnonymous extends ArFSDAOType { } const edgeOfFirstDrive = edges[0]; + const driveOwnerAddress = edgeOfFirstDrive.node.owner.address; return new ArweaveAddress(driveOwnerAddress); diff --git a/tests/integration/ardrive.int.test.ts b/tests/integration/ardrive.int.test.ts index 165203df..cc15ebdd 100644 --- a/tests/integration/ardrive.int.test.ts +++ b/tests/integration/ardrive.int.test.ts @@ -126,7 +126,7 @@ describe('ArDrive class - integrated', () => { }); it('throws an error if the owner of the drive conflicts with supplied wallet', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(unexpectedOwner); await expectAsyncErrorThrow({ promiseToError: arDrive.createPublicFolder({ @@ -139,7 +139,7 @@ describe('ArDrive class - integrated', () => { }); it('throws an error if the folder name conflicts with another ENTITY name in the destination folder', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); await expectAsyncErrorThrow({ promiseToError: arDrive.createPublicFolder({ @@ -152,7 +152,7 @@ describe('ArDrive class - integrated', () => { }); it('returns the correct ArFSResult', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); stub(arfsDao, 'getPublicDrive').resolves(stubPublicDrive); const result = await arDrive.createPublicFolder({ @@ -170,7 +170,7 @@ describe('ArDrive class - integrated', () => { }); it('throws an error if the owner of the drive conflicts with supplied wallet', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(unexpectedOwner); await expectAsyncErrorThrow({ promiseToError: arDrive.createPrivateFolder({ @@ -184,7 +184,7 @@ describe('ArDrive class - integrated', () => { }); it('throws an error if the folder name conflicts with another ENTITY name in the destination folder', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); await expectAsyncErrorThrow({ promiseToError: arDrive.createPrivateFolder({ @@ -199,7 +199,7 @@ describe('ArDrive class - integrated', () => { it('returns the correct ArFSResult', async () => { stub(arfsDao, 'getPrivateDrive').resolves(stubPrivateDrive); - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); const stubDriveKey = await getStubDriveKey(); const result = await arDrive.createPrivateFolder({ @@ -474,7 +474,7 @@ describe('ArDrive class - integrated', () => { }); it('throws an error if the owner of the drive conflicts with supplied wallet', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(unexpectedOwner); await expectAsyncErrorThrow({ promiseToError: arDrive.uploadPublicFile({ parentFolderId: stubEntityID, wrappedFile }), @@ -483,7 +483,7 @@ describe('ArDrive class - integrated', () => { }); it('throws an error if destination folder has a conflicting FOLDER name', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); await expectAsyncErrorThrow({ promiseToError: arDrive.uploadPublicFile({ @@ -496,7 +496,7 @@ describe('ArDrive class - integrated', () => { }); it('returns the correct empty ArFSResult if destination folder has a conflicting FILE name and conflict resolution is set to skip', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); const result = await arDrive.uploadPublicFile({ parentFolderId: stubEntityID, @@ -513,7 +513,7 @@ describe('ArDrive class - integrated', () => { }); it('returns the correct ArFSResult revision if destination folder has a conflicting FILE name and conflict resolution is set to replace', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); const result = await arDrive.uploadPublicFile({ parentFolderId: stubEntityID, @@ -527,7 +527,7 @@ describe('ArDrive class - integrated', () => { }); it('returns an empty ArFSResult if destination folder has a conflicting FILE name and a matching last modified date and the conflict resolution is set to upsert', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); stub(wrappedFile, 'lastModifiedDate').get(() => matchingLastModifiedDate); const result = await arDrive.uploadPublicFile({ @@ -545,7 +545,7 @@ describe('ArDrive class - integrated', () => { }); it('returns the correct ArFSResult revision if destination folder has a conflicting FILE name and a different last modified date and the conflict resolution is set to upsert', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); stub(wrappedFile, 'lastModifiedDate').get(() => differentLastModifiedDate); const result = await arDrive.uploadPublicFile({ @@ -560,7 +560,7 @@ describe('ArDrive class - integrated', () => { }); it('returns the correct ArFSResult', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); const result = await arDrive.uploadPublicFile({ parentFolderId: stubEntityID, wrappedFile }); assertUploadFileExpectations(result, 3204, 166, 0, '1', 'public'); @@ -587,7 +587,7 @@ describe('ArDrive class - integrated', () => { }); it('throws an error if the owner of the drive conflicts with supplied wallet', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(unexpectedOwner); await expectAsyncErrorThrow({ promiseToError: arDrive.uploadPrivateFile({ @@ -600,7 +600,7 @@ describe('ArDrive class - integrated', () => { }); it('throws an error if destination folder has a conflicting FOLDER name', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); await expectAsyncErrorThrow({ promiseToError: arDrive.uploadPrivateFile({ @@ -614,7 +614,7 @@ describe('ArDrive class - integrated', () => { }); it('returns the correct empty ArFSResult if destination folder has a conflicting FILE name and conflict resolution is set to skip', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); const result = await arDrive.uploadPrivateFile({ parentFolderId: stubEntityID, @@ -632,7 +632,7 @@ describe('ArDrive class - integrated', () => { }); it('returns the correct ArFSResult revision with if destination folder has a conflicting FILE name and conflict resolution is set to replace', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); const result = await arDrive.uploadPrivateFile({ parentFolderId: stubEntityID, @@ -647,7 +647,7 @@ describe('ArDrive class - integrated', () => { }); it('returns an empty ArFSResult if destination folder has a conflicting FILE name and a matching last modified date and the conflict resolution is set to upsert', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); stub(wrappedFile, 'lastModifiedDate').get(() => matchingLastModifiedDate); const result = await arDrive.uploadPrivateFile({ @@ -666,7 +666,7 @@ describe('ArDrive class - integrated', () => { }); it('returns the correct ArFSResult revision if destination folder has a conflicting FILE name and a different last modified date and the conflict resolution is set to upsert', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); stub(wrappedFile, 'lastModifiedDate').get(() => differentLastModifiedDate); const result = await arDrive.uploadPrivateFile({ @@ -682,7 +682,7 @@ describe('ArDrive class - integrated', () => { }); it('returns the correct ArFSResult', async () => { - stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner); + stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner); const stubDriveKey = await getStubDriveKey(); const result = await arDrive.uploadPrivateFile({