diff --git a/.changeset/brown-experts-lay.md b/.changeset/brown-experts-lay.md new file mode 100644 index 000000000..3b8c4205a --- /dev/null +++ b/.changeset/brown-experts-lay.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +differentiate between Record creator and author diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index b63bd8450..30ec77d9b 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -227,8 +227,10 @@ export class Record implements RecordModel { // Private variables for DWN `RecordsWrite` message properties. - /** The DID of the entity that authored the record. */ + /** The DID of the entity that most recently authored or deleted the record. */ private _author: string; + /** The DID of the entity that originally created the record. */ + private _creator: string; /** Attestation JWS signature. */ private _attestation?: DwnMessage[DwnInterface.RecordsWrite]['attestation']; /** Authorization signature(s). */ @@ -314,6 +316,9 @@ export class Record implements RecordModel { /** DID that is the logical author of the Record. */ get author(): string { return this._author; } + /** DID that is the original creator of the Record. */ + get creator(): string { return this._creator; } + /** Record's modified date */ get dateModified() { return this._descriptor.messageTimestamp; } @@ -368,6 +373,8 @@ export class Record implements RecordModel { // Store the author DID that originally signed the message as a convenience for developers, so // that they don't have to decode the signer's DID from the JWS. this._author = options.author; + // The creator is the author of the initial write, or the author of the record if there is no initial write. + this._creator = options.initialWrite ? getRecordAuthor(options.initialWrite) : options.author; // Store the `connectedDid`, and optionally the `delegateDid` and `permissionsApi` in order to be able // to perform operations on the record (update, delete, data) as a delegate of the connected DID. diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 4b4807814..c57829f50 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -2403,6 +2403,51 @@ describe('Record', () => { }); describe('update()', () => { + let notesProtocol: DwnProtocolDefinition; + + beforeEach(async () => { + const protocolUri = `http://example.com/notes-${TestDataGenerator.randomString(15)}`; + + notesProtocol = { + published : true, + protocol : protocolUri, + types : { + note: { + schema: 'http://example.com/note' + }, + request: { + schema: 'http://example.com/request' + } + }, + structure: { + request: { + $actions: [{ + who : 'anyone', + can : ['create', 'update', 'delete'] + },{ + who : 'recipient', + of : 'request', + can : ['co-update'] + }] + }, + note: { + } + } + }; + + // alice and bob both configure the protocol + const { status: aliceConfigStatus, protocol: aliceNotesProtocol } = await dwnAlice.protocols.configure({ message: { definition: notesProtocol } }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceNotesProtocolSend } = await aliceNotesProtocol.send(aliceDid.uri); + expect(aliceNotesProtocolSend.code).to.equal(202); + + const { status: bobConfigStatus, protocol: bobNotesProtocol } = await dwnBob.protocols.configure({ message: { definition: notesProtocol } }); + expect(bobConfigStatus.code).to.equal(202); + const { status: bobNotesProtocolSend } = await bobNotesProtocol!.send(bobDid.uri); + expect(bobNotesProtocolSend.code).to.equal(202); + + }); + it('updates a local record on the local DWN', async () => { const { status, record } = await dwnAlice.records.write({ data : 'Hello, world!', @@ -2906,6 +2951,64 @@ describe('Record', () => { expect(record.dataFormat).to.equal('application/json'); expect(await record.data.json()).to.deep.equal({ subject: 'another subject', body: 'another body' }); }); + + it('differentiates between creator and author', async () => { + const { status, record } = await dwnAlice.records.write({ + data : 'Hello, Bob!', + message : { + recipient : bobDid.uri, + protocol : notesProtocol.protocol, + protocolPath : 'request', + schema : notesProtocol.types.request.schema, + } + }); + expect(status.code).to.equal(202, 'create'); + expect(record).to.not.be.undefined; + const { status: sendStatus } = await record.send(); + expect(sendStatus.code).to.equal(202, 'send'); + + // bob reads the record + const readResult = await dwnBob.records.read({ + protocol : notesProtocol.protocol, + from : aliceDid.uri, + message : { + filter: { + recordId: record.id + } + } + }); + expect(readResult.status.code).to.equal(200, 'bob reads record'); + expect(readResult.record).to.not.be.undefined; + + const bobRecord = readResult.record; + const { status: storeStatus } = await bobRecord!.store(); + expect(storeStatus.code).to.equal(202, 'store'); + const { status: updateStatus } = await bobRecord.update({ data: 'Hello, Alice!' }); + expect(updateStatus.code).to.equal(202, 'update'); + + const updatedData = await bobRecord.send(aliceDid.uri); + expect(updatedData.status.code).to.equal(202, 'send update'); + + // alice reads the record + const readResultAlice = await dwnAlice.records.read({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + recordId: record.id + } + } + }); + + expect(readResultAlice.status.code).to.equal(200, 'alice reads record'); + expect(readResultAlice.record).to.not.be.undefined; + expect(await readResultAlice.record!.data.text()).to.equal('Hello, Alice!'); + + // alice is the creator + expect(readResultAlice.record!.creator).to.equal(aliceDid.uri); + // bob is the author + expect(readResultAlice.record!.author).to.equal(bobDid.uri); + }); }); describe('delete()', () => {