diff --git a/README.md b/README.md index 307076742..afc3948e9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Decentralized Web Node (DWN) SDK Code Coverage -![Statements](https://img.shields.io/badge/statements-94.36%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.95%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-92.3%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.36%25-brightgreen.svg?style=flat) +![Statements](https://img.shields.io/badge/statements-94.58%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.7%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-92.34%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.58%25-brightgreen.svg?style=flat) ## Introduction diff --git a/package-lock.json b/package-lock.json index 6a59a52ab..ad028ab65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tbd54566975/dwn-sdk-js", - "version": "0.0.22", + "version": "0.0.23", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@tbd54566975/dwn-sdk-js", - "version": "0.0.22", + "version": "0.0.23", "license": "Apache-2.0", "dependencies": { "@ipld/dag-cbor": "7.0.1", diff --git a/package.json b/package.json index 7e842e1f0..05f5cf039 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tbd54566975/dwn-sdk-js", - "version": "0.0.22", + "version": "0.0.23", "description": "A reference implementation of https://identity.foundation/decentralized-web-node/spec/", "type": "module", "types": "./dist/esm/src/index.d.ts", @@ -132,4 +132,4 @@ "url": "https://github.com/TBD54566975/dwn-sdk-js/issues" }, "homepage": "https://github.com/TBD54566975/dwn-sdk-js#readme" -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 588669142..302f18585 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ export type { HooksWriteMessage } from './interfaces/hooks/types.js'; export type { ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureMessage, ProtocolsQueryMessage } from './interfaces/protocols/types.js'; export type { RecordsDeleteMessage, RecordsQueryMessage, RecordsWriteMessage } from './interfaces/records/types.js'; export { AllowAllTenantGate, TenantGate } from './core/tenant-gate.js'; +export { Cid } from './utils/cid.js'; export { DataStore } from './store/data-store.js'; export { DateSort } from './interfaces/records/messages/records-query.js'; export { DataStream } from './utils/data-stream.js'; diff --git a/src/interfaces/records/handlers/records-write.ts b/src/interfaces/records/handlers/records-write.ts index cd706a2f7..3131df73d 100644 --- a/src/interfaces/records/handlers/records-write.ts +++ b/src/interfaces/records/handlers/records-write.ts @@ -54,7 +54,7 @@ export class RecordsWriteHandler implements MethodHandler { const newMessageIsInitialWrite = await recordsWrite.isInitialWrite(); if (!newMessageIsInitialWrite) { try { - const initialWrite = RecordsWrite.getInitialWrite(existingMessages); + const initialWrite = await RecordsWrite.getInitialWrite(existingMessages); RecordsWrite.verifyEqualityOfImmutableProperties(initialWrite, incomingMessage); } catch (e) { return new MessageReply({ diff --git a/src/interfaces/records/messages/records-write.ts b/src/interfaces/records/messages/records-write.ts index ffba7a805..7a5397484 100644 --- a/src/interfaces/records/messages/records-write.ts +++ b/src/interfaces/records/messages/records-write.ts @@ -11,7 +11,7 @@ import { ProtocolAuthorization } from '../../../core/protocol-authorization.js'; import { removeUndefinedProperties } from '../../../utils/object.js'; import { authorize, validateAuthorizationIntegrity } from '../../../core/auth.js'; -import { computeCid, computeDagPbCid } from '../../../utils/cid.js'; +import { Cid, computeCid } from '../../../utils/cid.js'; import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js'; import { GeneralJws, SignatureInput } from '../../../jose/jws/general/types.js'; @@ -73,7 +73,7 @@ export class RecordsWrite extends Message { /** * Creates a RecordsWrite message. * @param options.recordId If `undefined`, will be auto-filled as a originating message as convenience for developer. - * @param options.data Readable stream of the data to be stored. Must specify `option.dataCid` if `undefined`. + * @param options.data Data used to compute the `dataCid`. Must specify `option.dataCid` if `undefined`. * @param options.dataCid CID of the data that is already stored in the DWN. Must specify `option.data` if `undefined`. * @param options.dateCreated If `undefined`, it will be auto-filled with current time. * @param options.dateModified If `undefined`, it will be auto-filled with current time. @@ -85,7 +85,7 @@ export class RecordsWrite extends Message { options.data !== undefined && options.dataCid !== undefined) { throw new Error('one and only one parameter between `data` and `dataCid` is allowed'); } - const dataCid = options.dataCid ?? await computeDagPbCid(options.data); + const dataCid = options.dataCid ?? await Cid.computeDagPbCidFromBytes(options.data!); const descriptor: RecordsWriteDescriptor = { interface : DwnInterfaceName.Records, @@ -177,7 +177,7 @@ export class RecordsWrite extends Message { // inherit published value from parent if neither published nor datePublished is specified const published = options.published ?? (options.datePublished ? true : unsignedMessage.descriptor.published); // use current time if published but no explicit time given - let datePublished = undefined; + let datePublished: string | undefined = undefined; // if given explicitly published dated if (options.datePublished) { datePublished = options.datePublished; @@ -396,14 +396,14 @@ export class RecordsWrite extends Message { /** * Gets the initial write from the given list or record write. */ - public static getInitialWrite(messages: BaseMessage[]): RecordsWriteMessage{ + public static async getInitialWrite(messages: BaseMessage[]): Promise{ for (const message of messages) { - if (RecordsWrite.isInitialWrite(message)) { + if (await RecordsWrite.isInitialWrite(message)) { return message as RecordsWriteMessage; } } - throw new Error(`initial write is not found `); + throw new Error(`initial write is not found`); } /** diff --git a/src/utils/cid.ts b/src/utils/cid.ts index 4607e7a5d..59ac0143d 100644 --- a/src/utils/cid.ts +++ b/src/utils/cid.ts @@ -2,6 +2,7 @@ import * as cbor from '@ipld/dag-cbor'; import { CID } from 'multiformats/cid'; import { importer } from 'ipfs-unixfs-importer'; +import { Readable } from 'stream'; import { sha256 } from 'multiformats/hashes/sha2'; // a map of all supported CID hashing algorithms. This map is used to select the appropriate hasher @@ -16,20 +17,6 @@ const codecs = { [cbor.code]: cbor }; - -/** - * @returns V1 CID of the DAG comprised by chunking data into unixfs dag-pb encoded blocks - */ -export async function computeDagPbCid(content: Uint8Array): Promise { - const asyncDataBlocks = importer([{ content }], undefined, { onlyHash: true, cidVersion: 1 }); - - // NOTE: the last block contains the root CID - let block; - for await (block of asyncDataBlocks) { ; } - - return block.cid.toString(); -} - /** * Computes a V1 CID for the provided payload * @param payload @@ -71,3 +58,35 @@ export function parseCid(str: string): CID { return cid; } + + +/** + * Utility class for creating CIDs. Exported for the convenience of developers. + */ +export class Cid { + /** + * @returns V1 CID of the DAG comprised by chunking data into unixfs DAG-PB encoded blocks + */ + public static async computeDagPbCidFromBytes(content: Uint8Array): Promise { + const asyncDataBlocks = importer([{ content }], undefined, { onlyHash: true, cidVersion: 1 }); + + // NOTE: the last block contains the root CID + let block; + for await (block of asyncDataBlocks) { ; } + + return block.cid.toString(); + } + + /** + * @returns V1 CID of the DAG comprised by chunking data into unixfs DAG-PB encoded blocks + */ + public static async computeDagPbCidFromStream(dataStream: Readable): Promise { + const asyncDataBlocks = importer([{ content: dataStream }], undefined, { onlyHash: true, cidVersion: 1 }); + + // NOTE: the last block contains the root CID + let block; + for await (block of asyncDataBlocks) { ; } + + return block.cid.toString(); + } +} diff --git a/tests/utils/cid.spec.ts b/tests/utils/cid.spec.ts index 06fd4bdda..baae39b3f 100644 --- a/tests/utils/cid.spec.ts +++ b/tests/utils/cid.spec.ts @@ -4,20 +4,42 @@ import * as cbor from '@ipld/dag-cbor'; import chaiAsPromised from 'chai-as-promised'; import chai, { expect } from 'chai'; -import { computeCid } from '../../src/utils/cid.js'; +import { DataStream } from '../../src/index.js'; import { sha256 } from 'multiformats/hashes/sha2'; import { TestDataGenerator } from '../utils/test-data-generator.js'; +import { Cid, computeCid, parseCid } from '../../src/utils/cid.js'; // extend chai to test promises chai.use(chaiAsPromised); describe('CID', () => { + it('should yield the same CID using either computeDagPbCidFromBytes() & computeDagPbCidFromStream()', async () => { + const randomBytes = TestDataGenerator.randomBytes(500_000); + const randomByteStream = await DataStream.fromBytes(randomBytes); + + const cid1 = await Cid.computeDagPbCidFromBytes(randomBytes); + const cid2 = await Cid.computeDagPbCidFromStream(randomByteStream); + expect(cid1).to.equal(cid2); + }); + describe('computeCid', () => { - xit('throws an error if codec is not supported'); - xit('throws an error if multihasher is not supported'); - xit('generates a cbor/sha256 v1 cid by default'); + it('throws an error if codec is not supported', async () => { + const anyTestData = { + a: TestDataGenerator.randomString(32), + }; + const computeCidPromise = computeCid(anyTestData, 'unknownCodec'); + await expect(computeCidPromise).to.be.rejectedWith('codec [unknownCodec] not supported'); + }); - it(' should generate a CBOR SHA256 CID identical to IPFS block encoding algorithm', async () => { + it('throws an error if multihasher is not supported', async () => { + const anyTestData = { + a: TestDataGenerator.randomString(32), + }; + const computeCidPromise = computeCid(anyTestData, '113', 'unknownHashingAlgorithm'); // 113 = CBOR + await expect(computeCidPromise).to.be.rejectedWith('multihash code [unknownHashingAlgorithm] not supported'); + }); + + it('should by default generate a CBOR SHA256 CID identical to IPFS block encoding algorithm', async () => { const anyTestData = { a : TestDataGenerator.randomString(32), b : TestDataGenerator.randomString(32), @@ -49,8 +71,12 @@ describe('CID', () => { }); describe('parseCid', () => { - xit('throws an error if codec is not supported'); - xit('throws an error if multihasher is not supported'); - xit('parses provided str into a V1 cid'); + it('throws an error if codec is not supported', async () => { + expect(() => parseCid('bafybeihzdcfjv55kxiz7sxwxaxbnjgj7rm2amvrxpi67jpwkgygjzoh72y')).to.throw('codec [112] not supported'); // a DAG-PB CID + }); + + it('throws an error if multihasher is not supported', async () => { + expect(() => parseCid('bafy2bzacec2qlo3cohxyaoulipd3hurlq6pspvmpvmnmqsxfg4vbumpq3ufag')).to.throw('multihash code [45600] not supported'); // 45600 = BLAKE2b-256 CID + }); }); }); \ No newline at end of file