import jsonpatch from 'fast-json-patch' ; import cloneDeep from 'lodash.clonedeep' ; import { TileDocument } from '@ceramicnetwork/stream-tile' ; import { AnchorStatus , CommitData , CommitType , Context , SignatureStatus , StreamConstructor , StreamHandler , StreamState , StreamUtils , } from '@ceramicnetwork/common' ; import { StreamID } from '@ceramicnetwork/streamid' ; function stringArraysEqual ( arr1 , arr2 ) { if ( arr1 . length != arr2 . length ) { return false ; } for ( let i = 0 ; i < arr1 . length ; i ++ ) { if ( arr1 [ i ] !== arr2 [ i ] ) { return false ; } } return true ; } /**
 * TileDocument stream handler implementation
 */ export class TileDocumentHandler implements StreamHandler < TileDocument > { get type ( ) : number { return TileDocument . STREAM_TYPE_ID ; } get name ( ) : string { return TileDocument . STREAM_TYPE_NAME ; } get stream_constructor ( ) : StreamConstructor < TileDocument > { return TileDocument ; } param commitData param context param state async applyCommit ( commitData , context , state ? ) { if ( state == null ) { return this . _applyGenesis ( commitData , context ) ; } if ( StreamUtils . isAnchorCommitData ( commitData ) ) { return this . _applyAnchor ( context , commitData , state ) ; } return this . _applySigned ( commitData , state , context ) ; } param commitData param context private async _applyGenesis ( commitData , context ) { const payload = commitData . commit ; const isSigned = StreamUtils . isSignedCommitData ( commitData ) ; if ( isSigned ) { await this . _verifySignature ( commitData , context , payload . header . controllers [ 0 ] ) ; } else if ( payload . data ) { throw Error ( 'Genesis commit with contents should always be signed' ) ; } if ( ! ( payload . header . controllers && payload . header . controllers . length === 1 ) ) { throw new Error ( 'Exactly one controller must be specified' ) ; } return { type : TileDocument . STREAM_TYPE_ID , content : payload . data || { } , metadata : payload . header , signature : isSigned ? SignatureStatus . SIGNED : SignatureStatus . GENESIS , anchorStatus : AnchorStatus . NOT_REQUESTED , log : [ { cid : commitData . cid , type : CommitType . GENESIS } ] , } ; } param commitData param state param context private async _applySigned ( commitData , state , context ) { const controller = state . next ?. metadata ?. controllers ?. [ 0 ] || state . metadata . controllers [ 0 ] ; await this . _verifySignature ( commitData , context , controller ) ; const payload = commitData . commit ; if ( ! payload . id . equals ( state . log [ 0 ] . cid ) ) { throw new Error ( `Invalid streamId ${ payload . id }, expected ${ state . log [ 0 ] . cid }` ) ; } if ( payload . header . controllers && payload . header . controllers . length !== 1 ) { throw new Error ( 'Exactly one controller must be specified' ) ; } if ( state . metadata . forbidControllerChange && payload . header . controllers && ! stringArraysEqual ( payload . header . controllers , state . metadata . controllers ) ) { const streamId = new StreamID ( TileDocument . STREAM_TYPE_ID , state . log [ 0 ] . cid ) ; throw new Error ( `Cannot change controllers since 'forbidControllerChange' is set. Tried to change controllers for Stream ${ streamId } from ${ JSON . stringify ( state . metadata . controllers ) } to ${ payload . header . controllers }` ) ; } if ( payload . header . forbidControllerChange !== undefined && payload . header . forbidControllerChange !== state . metadata . forbidControllerChange ) { throw new Error ( "Changing 'forbidControllerChange' metadata property is not allowed" ) ; } const nextState = cloneDeep ( state ) ; nextState . signature = SignatureStatus . SIGNED ; nextState . anchorStatus = AnchorStatus . NOT_REQUESTED ; nextState . log . push ( { cid : commitData . cid , type : CommitType . SIGNED } ) ; const content = state . next ?. content ?? state . content ; const metadata = state . next ?. metadata ?? state . metadata ; nextState . next = { content : jsonpatch . applyPatch ( content , payload . data ) . newDocument , metadata : { ... metadata , ... payload . header } , } ; return nextState ; } param context param commitData param state private async _applyAnchor ( context , commitData , state ) { const proof = commitData . proof ; state . log . push ( { cid : commitData . cid , type : CommitType . ANCHOR , timestamp : proof . blockTimestamp } ) ; let content = state . content ; let metadata = state . metadata ; if ( state . next ?. content ) { content = state . next . content ; delete state . next . content ; } if ( state . next ?. metadata ) { metadata = state . next . metadata ; delete state . next . metadata ; } delete state . next ; delete state . anchorScheduledFor ; return { ... state , content , metadata , anchorStatus : AnchorStatus . ANCHORED , anchorProof : proof , } ; } param commitData param context param controller private async _verifySignature ( commitData , context , controller ) { await context . did . verifyJWS ( commitData . envelope , { atTime : commitData . timestamp , issuer : controller , disableTimecheck : commitData . disableTimecheck , } ) ; } }