diff --git a/package-lock.json b/package-lock.json index f527de4..2d3d4fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "dwn-server", "version": "0.0.2", "dependencies": { - "@tbd54566975/dwn-sdk-js": "0.0.32", + "@tbd54566975/dwn-sdk-js": "0.0.33", "bytes": "3.1.2", "cors": "2.8.5", "express": "4.18.2", @@ -326,9 +326,9 @@ "integrity": "sha512-aWItSZvJj4+GI6FWkjZR13xPNPctq2RRakzo+O6vN7bC2yjwdg5EFpgaSAUn95b7BGSgcflvzVDPoKmJv24IOg==" }, "node_modules/@tbd54566975/dwn-sdk-js": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.32.tgz", - "integrity": "sha512-SijEHpmJDa0I9hC7jn/8P7XeYeFAi9byMc+b36yuCPDiF+j/hLLYGMpZpnbylJ6+ldWqWWEunumeWjxU4zrc3g==", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.33.tgz", + "integrity": "sha512-ZdQtRTd0M2VcgGli7kDzkePsuxpwiOg+PRsBJ/UPC5fc6oflzbRqV6Lg9v8bWHozjdnijP58J3RMWmf4cj7bWw==", "dependencies": { "@ipld/dag-cbor": "9.0.0", "@js-temporal/polyfill": "0.4.3", @@ -5891,9 +5891,9 @@ "integrity": "sha512-aWItSZvJj4+GI6FWkjZR13xPNPctq2RRakzo+O6vN7bC2yjwdg5EFpgaSAUn95b7BGSgcflvzVDPoKmJv24IOg==" }, "@tbd54566975/dwn-sdk-js": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.32.tgz", - "integrity": "sha512-SijEHpmJDa0I9hC7jn/8P7XeYeFAi9byMc+b36yuCPDiF+j/hLLYGMpZpnbylJ6+ldWqWWEunumeWjxU4zrc3g==", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.33.tgz", + "integrity": "sha512-ZdQtRTd0M2VcgGli7kDzkePsuxpwiOg+PRsBJ/UPC5fc6oflzbRqV6Lg9v8bWHozjdnijP58J3RMWmf4cj7bWw==", "requires": { "@ipld/dag-cbor": "9.0.0", "@js-temporal/polyfill": "0.4.3", diff --git a/package.json b/package.json index a91a79d..4ff2a54 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "version": "0.0.2", "dependencies": { - "@tbd54566975/dwn-sdk-js": "0.0.32", + "@tbd54566975/dwn-sdk-js": "0.0.33", "bytes": "3.1.2", "node-fetch": "3.3.1", "cors": "2.8.5", diff --git a/src/json-rpc-handlers/dwn/process-message.ts b/src/json-rpc-handlers/dwn/process-message.ts index 2f5a088..d4c7f8c 100644 --- a/src/json-rpc-handlers/dwn/process-message.ts +++ b/src/json-rpc-handlers/dwn/process-message.ts @@ -2,17 +2,29 @@ import type { Readable as IsomorphicReadable } from 'readable-stream'; import type { JsonRpcHandler, HandlerResponse } from '../../lib/json-rpc-router.js'; import { v4 as uuidv4 } from 'uuid'; +import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; import { JsonRpcErrorCodes, createJsonRpcErrorResponse, createJsonRpcSuccessResponse } from '../../lib/json-rpc.js'; export const handleDwnProcessMessage: JsonRpcHandler = async (dwnRequest, context) => { let { dwn, dataStream } = context; const { target, message } = dwnRequest.params; - const requestId = dwnRequest.id ?? uuidv4(); try { - const reply = await dwn.processMessage(target, message, dataStream as IsomorphicReadable); + let reply; + const messageType = message?.descriptor?.interface + message?.descriptor?.method; + + // When a record is deleted via `RecordsDelete`, the initial RecordsWrite is kept as a tombstone _in addition_ + // to the RecordsDelete message. the data associated to that initial RecordsWrite is deleted. If a record was written + // _and_ deleted before it ever got to dwn-server, we end up in a situation where we still need to process the tombstone + // so that we can process the RecordsDelete. + if (messageType === DwnInterfaceName.Records + DwnMethodName.Write && !dataStream) { + reply = await dwn.synchronizePrunedInitialRecordsWrite(target, message); + } else { + reply = await dwn.processMessage(target, message, dataStream as IsomorphicReadable); + } + // RecordsRead messages return record data as a stream to for accommodate large amounts of data let recordDataStream; diff --git a/tests/http-api.spec.ts b/tests/http-api.spec.ts index 27b5336..917dade 100644 --- a/tests/http-api.spec.ts +++ b/tests/http-api.spec.ts @@ -57,20 +57,27 @@ describe('http api', function() { it('responds with a 2XX HTTP status if JSON RPC handler returns 4XX/5XX DWN status code', async function() { const alice = await createProfile(); + const { recordsWrite, dataStream } = await createRecordsWriteMessage(alice); + + // Intentionally delete a required property to produce an invalid RecordsWrite message. + const message = recordsWrite.toJSON(); + delete message['descriptor']['interface']; const requestId = uuidv4(); - const { recordsWrite } = await createRecordsWriteMessage(alice); const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { - message : recordsWrite.toJSON(), + message : message, target : alice.did, }); - // Attempt an initial RecordsWrite without any data to ensure the DWN returns an error. + const dataBytes = await DataStream.toBytes(dataStream); + + // Attempt an initial RecordsWrite with the invalid message to ensure the DWN returns an error. let responseInitialWrite = await fetch('http://localhost:3000', { method : 'POST', headers : { 'dwn-request': JSON.stringify(dwnRequest) - } + }, + body: new Blob([dataBytes]) }); expect(responseInitialWrite.status).to.equal(200); @@ -79,9 +86,10 @@ describe('http api', function() { expect(body.id).to.equal(requestId); expect(body.error).to.not.exist; + const { reply } = body.result; expect(reply.status.code).to.equal(400); - expect(reply.status.detail).to.include('RecordsWriteMissingDataStream'); + expect(reply.status.detail).to.include('Both interface and method must be present'); }); it('exposes dwn-response header', async function() { @@ -216,6 +224,26 @@ describe('http api', function() { const { reply } = body.result; expect(reply.status.code).to.equal(202); }); + + it('handles a RecordsWrite tombstone', async function() { + const alice = await createProfile(); + const { recordsWrite: tombstone } = await createRecordsWriteMessage(alice); + + let requestId = uuidv4(); + let dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + message : tombstone.toJSON(), + target : alice.did + }); + + let responeTombstone = await fetch('http://localhost:3000', { + method : 'POST', + headers : { + 'dwn-request': JSON.stringify(dwnRequest) + }, + }); + + expect(responeTombstone.status).to.equal(200); + }); }); describe('health check', function() {