11import type { Readable } from '@web5/common' ;
2- import type { DwnConfig , GenericMessage } from '@tbd54566975/dwn-sdk-js' ;
2+
3+ import {
4+ Cid ,
5+ DataEncodedRecordsWriteMessage ,
6+ DataStoreLevel ,
7+ Dwn ,
8+ DwnConfig ,
9+ DwnMethodName ,
10+ EventLogLevel ,
11+ GenericMessage ,
12+ Message ,
13+ MessageStoreLevel ,
14+ PermissionGrant ,
15+ PermissionScope ,
16+ PermissionsProtocol ,
17+ RecordsWrite ,
18+ ResumableTaskStoreLevel
19+ } from '@tbd54566975/dwn-sdk-js' ;
320
421import { NodeStream } from '@web5/common' ;
522import { utils as cryptoUtils } from '@web5/crypto' ;
623import { DidDht , DidJwk , DidResolverCacheLevel , UniversalResolver } from '@web5/dids' ;
7- import { Cid , DataStoreLevel , Dwn , DwnMethodName , EventLogLevel , Message , MessageStoreLevel , ResumableTaskStoreLevel } from '@tbd54566975/dwn-sdk-js' ;
824
925import type { Web5PlatformAgent } from './types/agent.js' ;
10- import type { DwnMessage , DwnMessageReply , DwnMessageWithData , DwnResponse , DwnSigner , MessageHandler , ProcessDwnRequest , SendDwnRequest } from './types/dwn.js' ;
26+ import type {
27+ DwnMessage ,
28+ DwnMessageInstance ,
29+ DwnMessageParams ,
30+ DwnMessageReply ,
31+ DwnMessageWithData ,
32+ DwnRecordsInterfaces ,
33+ DwnResponse ,
34+ DwnSigner ,
35+ MessageHandler ,
36+ ProcessDwnRequest ,
37+ SendDwnRequest
38+ } from './types/dwn.js' ;
1139
1240import { DwnInterface , dwnMessageConstructors } from './types/dwn.js' ;
1341import { blobToIsomorphicNodeReadable , getDwnServiceEndpointUrls , isRecordsWrite , webReadableToIsomorphicNodeReadable } from './utils.js' ;
42+ import { DwnPermissionsUtil } from './dwn-permissions-util.js' ;
1443
1544export type DwnMessageWithBlob < T extends DwnInterface > = {
1645 message : DwnMessage [ T ] ;
@@ -39,6 +68,14 @@ export function isDwnMessage<T extends DwnInterface>(
3968 return incomingMessageInterfaceName === messageType ;
4069}
4170
71+ export function isRecordsType ( messageType : DwnInterface ) : messageType is DwnRecordsInterfaces {
72+ return messageType === DwnInterface . RecordsDelete ||
73+ messageType === DwnInterface . RecordsQuery ||
74+ messageType === DwnInterface . RecordsRead ||
75+ messageType === DwnInterface . RecordsSubscribe ||
76+ messageType === DwnInterface . RecordsWrite ;
77+ }
78+
4279export class AgentDwnApi {
4380 /**
4481 * Holds the instance of a `Web5PlatformAgent` that represents the current execution context for
@@ -255,10 +292,15 @@ export class AgentDwnApi {
255292 private async constructDwnMessage < T extends DwnInterface > ( { request } : {
256293 request : ProcessDwnRequest < T >
257294 } ) : Promise < DwnMessageWithData < T > > {
295+ // if the request has a granteeDid, ensure the messageParams include the proper grant parameters
296+ if ( request . granteeDid && ! this . hasGrantParams ( request . messageParams ) ) {
297+ throw new Error ( 'AgentDwnApi: Requested to sign with a permission but no grant messageParams were provided in the request' ) ;
298+ }
299+
258300 const rawMessage = request . rawMessage ;
259301 let readableStream : Readable | undefined ;
260-
261302 // TODO: Consider refactoring to move data transformations imposed by fetch() limitations to the HTTP transport-related methods.
303+ // if the request is a RecordsWrite message, we need to handle the data stream and update the messageParams accordingly
262304 if ( isDwnRequest ( request , DwnInterface . RecordsWrite ) ) {
263305 const messageParams = request . messageParams ;
264306
@@ -285,23 +327,49 @@ export class AgentDwnApi {
285327 }
286328 }
287329
288- // Determine the signer for the message.
289- const signer = await this . getSigner ( request . author ) ;
290-
330+ let dwnMessage : DwnMessageInstance [ T ] ;
291331 const dwnMessageConstructor = dwnMessageConstructors [ request . messageType ] ;
292- const dwnMessage = rawMessage ? await dwnMessageConstructor . parse ( rawMessage ) : await dwnMessageConstructor . create ( {
293- // TODO: Implement alternative to type assertion.
294- ...request . messageParams ! ,
295- signer
296- } ) ;
297332
298- if ( isRecordsWrite ( dwnMessage ) && request . signAsOwner ) {
299- await dwnMessage . signAsOwner ( signer ) ;
333+ // if there is no raw message provided, we need to create the dwn message
334+ if ( ! rawMessage ) {
335+
336+ // If we need to sign as an author delegate or with permissions we need to get the grantee's signer
337+ // The messageParams should include either a permissionGrantId, or a delegatedGrant message
338+ const signer = request . granteeDid ?
339+ await this . getSigner ( request . granteeDid ) :
340+ await this . getSigner ( request . author ) ;
341+
342+ dwnMessage = await dwnMessageConstructor . create ( {
343+ // TODO: Implement alternative to type assertion.
344+ ...request . messageParams ! ,
345+ signer
346+ } ) ;
347+
348+ } else {
349+ dwnMessage = await dwnMessageConstructor . parse ( rawMessage ) ;
350+ if ( isRecordsWrite ( dwnMessage ) && request . signAsOwner ) {
351+ // if we are signing as owner, we use the author's signer
352+ const signer = await this . getSigner ( request . author ) ;
353+ await dwnMessage . signAsOwner ( signer ) ;
354+ } else if ( request . granteeDid && isRecordsWrite ( dwnMessage ) && request . signAsOwnerDelegate ) {
355+ // if we are signing as owner delegate, we use the grantee's signer and the provided delegated grant
356+ const signer = await this . getSigner ( request . granteeDid ) ;
357+
358+ //if we have reached here, the presence of the grant params has already been checked
359+ const messageParams = request . messageParams as DwnMessageParams [ DwnInterface . RecordsWrite ] ;
360+ await dwnMessage . signAsOwnerDelegate ( signer , messageParams . delegatedGrant ! ) ;
361+ }
300362 }
301363
302364 return { message : dwnMessage . message as DwnMessage [ T ] , dataStream : readableStream } ;
303365 }
304366
367+ private hasGrantParams < T extends DwnInterface > ( params ?: DwnMessageParams [ T ] ) : boolean {
368+ return params !== undefined &&
369+ ( ( 'permissionGrantId' in params && params . permissionGrantId !== undefined ) ||
370+ ( 'delegatedGrant' in params && params . delegatedGrant !== undefined ) ) ;
371+ }
372+
305373 private async getSigner ( author : string ) : Promise < DwnSigner > {
306374 // If the author is the Agent's DID, use the Agent's signer.
307375 if ( author === this . agent . agentDid . uri ) {
@@ -382,4 +450,106 @@ export class AgentDwnApi {
382450
383451 return dwnMessageWithBlob ;
384452 }
453+
454+ /**
455+ * NOTE EVERYTHING BELOW THIS LINE IS TEMPORARY
456+ * TODO: Create a `grants` API to handle creating permission requests, grants and revocations
457+ * */
458+
459+ /**
460+ * Performs a RecordsQuery for permission grants that match the given parameters.
461+ */
462+ public async fetchGrants ( { author, target, grantee, grantor } : {
463+ /** author of the query message, defaults to grantee */
464+ author ?: string ,
465+ /** target of the query message, defaults to author */
466+ target ?: string ,
467+ grantor : string ,
468+ grantee : string
469+ } ) : Promise < DataEncodedRecordsWriteMessage [ ] > {
470+ // if no author is provided, use the grantee's DID
471+ author ??= grantee ;
472+ // if no target is explicitly provided, use the author
473+ target ??= author ;
474+
475+ const { reply : grantsReply } = await this . processRequest ( {
476+ author,
477+ target,
478+ messageType : DwnInterface . RecordsQuery ,
479+ messageParams : {
480+ filter : {
481+ author : grantor , // the author of the grant would be the grantor and the logical author of the message
482+ recipient : grantee , // the recipient of the grant would be the grantee
483+ ...DwnPermissionsUtil . permissionsProtocolParams ( 'grant' )
484+ }
485+ }
486+ } ) ;
487+
488+ if ( grantsReply . status . code !== 200 ) {
489+ throw new Error ( `AgentDwnApi: Failed to fetch grants: ${ grantsReply . status . detail } ` ) ;
490+ }
491+
492+ return grantsReply . entries ! as DataEncodedRecordsWriteMessage [ ] ;
493+ } ;
494+
495+ /**
496+ * Check whether a grant is revoked by reading the revocation record for a given grant recordId.
497+ */
498+ public async isGrantRevoked ( author :string , target : string , grantRecordId : string ) : Promise < boolean > {
499+ const { reply : revocationReply } = await this . processRequest ( {
500+ author,
501+ target,
502+ messageType : DwnInterface . RecordsRead ,
503+ messageParams : {
504+ filter : {
505+ parentId : grantRecordId ,
506+ ...DwnPermissionsUtil . permissionsProtocolParams ( 'revoke' )
507+ }
508+ }
509+ } ) ;
510+
511+ if ( revocationReply . status . code === 404 ) {
512+ // no revocation found, the grant is not revoked
513+ return false ;
514+ } else if ( revocationReply . status . code === 200 ) {
515+ // a revocation was found, the grant is revoked
516+ return true ;
517+ }
518+
519+ throw new Error ( `AgentDwnApi: Failed to check if grant is revoked: ${ revocationReply . status . detail } ` ) ;
520+ }
521+
522+ public async createGrant ( { grantedFrom, dateExpires, grantedTo, scope, delegated } :{
523+ dateExpires : string ,
524+ grantedFrom : string ,
525+ grantedTo : string ,
526+ scope : PermissionScope ,
527+ delegated ?: boolean
528+ } ) : Promise < {
529+ recordsWrite : RecordsWrite ,
530+ dataEncodedMessage : DataEncodedRecordsWriteMessage ,
531+ permissionGrantBytes : Uint8Array
532+ } > {
533+ return await PermissionsProtocol . createGrant ( {
534+ signer : await this . getSigner ( grantedFrom ) ,
535+ grantedTo,
536+ dateExpires,
537+ scope,
538+ delegated
539+ } ) ;
540+ }
541+
542+ public async createRevocation ( { grant, author } :{
543+ author : string ,
544+ grant : PermissionGrant
545+ } ) : Promise < {
546+ recordsWrite : RecordsWrite ,
547+ dataEncodedMessage : DataEncodedRecordsWriteMessage ,
548+ permissionRevocationBytes : Uint8Array
549+ } > {
550+ return await PermissionsProtocol . createRevocation ( {
551+ signer : await this . getSigner ( author ) ,
552+ grant,
553+ } ) ;
554+ }
385555}
0 commit comments