@enbox/api 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +585 -0
  3. package/dist/browser.js +2226 -0
  4. package/dist/browser.js.map +7 -0
  5. package/dist/browser.mjs +2226 -0
  6. package/dist/browser.mjs.map +7 -0
  7. package/dist/cjs/did-api.js +126 -0
  8. package/dist/cjs/did-api.js.map +1 -0
  9. package/dist/cjs/dwn-api.js +804 -0
  10. package/dist/cjs/dwn-api.js.map +1 -0
  11. package/dist/cjs/grant-revocation.js +183 -0
  12. package/dist/cjs/grant-revocation.js.map +1 -0
  13. package/dist/cjs/index.js +63 -0
  14. package/dist/cjs/index.js.map +1 -0
  15. package/dist/cjs/package.json +1 -0
  16. package/dist/cjs/permission-grant.js +365 -0
  17. package/dist/cjs/permission-grant.js.map +1 -0
  18. package/dist/cjs/permission-request.js +272 -0
  19. package/dist/cjs/permission-request.js.map +1 -0
  20. package/dist/cjs/protocol.js +110 -0
  21. package/dist/cjs/protocol.js.map +1 -0
  22. package/dist/cjs/record.js +1127 -0
  23. package/dist/cjs/record.js.map +1 -0
  24. package/dist/cjs/subscription-util.js +86 -0
  25. package/dist/cjs/subscription-util.js.map +1 -0
  26. package/dist/cjs/utils.js +127 -0
  27. package/dist/cjs/utils.js.map +1 -0
  28. package/dist/cjs/vc-api.js +64 -0
  29. package/dist/cjs/vc-api.js.map +1 -0
  30. package/dist/cjs/web5.js +471 -0
  31. package/dist/cjs/web5.js.map +1 -0
  32. package/dist/esm/did-api.js +69 -0
  33. package/dist/esm/did-api.js.map +1 -0
  34. package/dist/esm/dwn-api.js +573 -0
  35. package/dist/esm/dwn-api.js.map +1 -0
  36. package/dist/esm/grant-revocation.js +109 -0
  37. package/dist/esm/grant-revocation.js.map +1 -0
  38. package/dist/esm/index.js +34 -0
  39. package/dist/esm/index.js.map +1 -0
  40. package/dist/esm/permission-grant.js +233 -0
  41. package/dist/esm/permission-grant.js.map +1 -0
  42. package/dist/esm/permission-request.js +166 -0
  43. package/dist/esm/permission-request.js.map +1 -0
  44. package/dist/esm/protocol.js +67 -0
  45. package/dist/esm/protocol.js.map +1 -0
  46. package/dist/esm/record.js +814 -0
  47. package/dist/esm/record.js.map +1 -0
  48. package/dist/esm/subscription-util.js +35 -0
  49. package/dist/esm/subscription-util.js.map +1 -0
  50. package/dist/esm/utils.js +120 -0
  51. package/dist/esm/utils.js.map +1 -0
  52. package/dist/esm/vc-api.js +30 -0
  53. package/dist/esm/vc-api.js.map +1 -0
  54. package/dist/esm/web5.js +281 -0
  55. package/dist/esm/web5.js.map +1 -0
  56. package/dist/types/did-api.d.ts +66 -0
  57. package/dist/types/did-api.d.ts.map +1 -0
  58. package/dist/types/dwn-api.d.ts +336 -0
  59. package/dist/types/dwn-api.d.ts.map +1 -0
  60. package/dist/types/grant-revocation.d.ts +66 -0
  61. package/dist/types/grant-revocation.d.ts.map +1 -0
  62. package/dist/types/index.d.ts +34 -0
  63. package/dist/types/index.d.ts.map +1 -0
  64. package/dist/types/permission-grant.d.ts +157 -0
  65. package/dist/types/permission-grant.d.ts.map +1 -0
  66. package/dist/types/permission-request.d.ts +108 -0
  67. package/dist/types/permission-request.d.ts.map +1 -0
  68. package/dist/types/protocol.d.ts +59 -0
  69. package/dist/types/protocol.d.ts.map +1 -0
  70. package/dist/types/record.d.ts +441 -0
  71. package/dist/types/record.d.ts.map +1 -0
  72. package/dist/types/subscription-util.d.ts +19 -0
  73. package/dist/types/subscription-util.d.ts.map +1 -0
  74. package/dist/types/utils.d.ts +85 -0
  75. package/dist/types/utils.d.ts.map +1 -0
  76. package/dist/types/vc-api.d.ts +24 -0
  77. package/dist/types/vc-api.d.ts.map +1 -0
  78. package/dist/types/web5.d.ts +219 -0
  79. package/dist/types/web5.d.ts.map +1 -0
  80. package/package.json +111 -0
  81. package/src/did-api.ts +90 -0
  82. package/src/dwn-api.ts +952 -0
  83. package/src/grant-revocation.ts +124 -0
  84. package/src/index.ts +35 -0
  85. package/src/permission-grant.ts +327 -0
  86. package/src/permission-request.ts +214 -0
  87. package/src/protocol.ts +87 -0
  88. package/src/record.ts +1125 -0
  89. package/src/subscription-util.ts +42 -0
  90. package/src/utils.ts +128 -0
  91. package/src/vc-api.ts +30 -0
  92. package/src/web5.ts +516 -0
package/src/record.ts ADDED
@@ -0,0 +1,1125 @@
1
+ /**
2
+ * NOTE: Added reference types here to avoid a `pnpm` bug during build.
3
+ * https://github.com/TBD54566975/web5-js/pull/507
4
+ */
5
+ /// <reference types="@enbox/dwn-sdk-js" />
6
+
7
+ import type { Readable } from '@enbox/common';
8
+ import {
9
+ Web5Agent,
10
+ DwnInterface,
11
+ DwnMessage,
12
+ DwnMessageParams,
13
+ DwnResponseStatus,
14
+ ProcessDwnRequest,
15
+ DwnMessageDescriptor,
16
+ getPaginationCursor,
17
+ getRecordAuthor,
18
+ DwnDateSort,
19
+ DwnPaginationCursor,
20
+ isDwnMessage,
21
+ SendDwnRequest,
22
+ PermissionsApi,
23
+ AgentPermissionsApi,
24
+ getRecordProtocolRole
25
+ } from '@enbox/agent';
26
+
27
+ import { Convert, isEmptyObject, NodeStream, removeUndefinedProperties, Stream } from '@enbox/common';
28
+
29
+ import { dataToBlob, SendCache } from './utils.js';
30
+ import { PermissionGrant } from './permission-grant.js';
31
+
32
+ /**
33
+ * Represents Immutable Record properties that cannot be changed after the record is created.
34
+ *
35
+ * @beta
36
+ * */
37
+ export type ImmutableRecordProperties =
38
+ Pick<DwnMessageDescriptor[DwnInterface.RecordsWrite], 'dateCreated' | 'parentId' | 'protocol' | 'protocolPath' | 'recipient' | 'schema'>;
39
+
40
+ /**
41
+ * Represents Optional Record properties that depend on the Record's current state.
42
+ *
43
+ * @beta
44
+ */
45
+ export type OptionalRecordProperties =
46
+ Pick<DwnMessage[DwnInterface.RecordsWrite], 'authorization' | 'attestation' | 'encryption' | 'contextId' > &
47
+ Pick<DwnMessageDescriptor[DwnInterface.RecordsWrite], 'dataFormat' | 'dataCid' | 'dataSize' | 'datePublished' | 'published' | 'tags'>;
48
+
49
+ /**
50
+ * Represents the structured data model of a record, encapsulating the essential fields that define
51
+ * the record's metadata and payload within a Decentralized Web Node (DWN).
52
+ *
53
+ * @beta
54
+ */
55
+ export type RecordModel = ImmutableRecordProperties & OptionalRecordProperties & {
56
+
57
+ /** The logical author of the record. */
58
+ author: string;
59
+
60
+ /** The unique identifier of the record. */
61
+ recordId?: string;
62
+
63
+ /** The timestamp indicating when the record was last modified. */
64
+ messageTimestamp?: string;
65
+
66
+ /** The protocol role under which this record is written. */
67
+ protocolRole?: RecordOptions['protocolRole'];
68
+ }
69
+
70
+ /**
71
+ * Options for configuring a {@link Record} instance, extending the base `RecordsWriteMessage` with
72
+ * additional properties.
73
+ *
74
+ * This type combines the standard fields required for writing DWN records with additional metadata
75
+ * and configuration options used specifically in the {@link Record} class.
76
+ *
77
+ * @beta
78
+ */
79
+ export type RecordOptions = DwnMessage[DwnInterface.RecordsWrite | DwnInterface.RecordsDelete] & {
80
+ /** The DID that signed the record. */
81
+ author: string;
82
+
83
+ /** The attestation signature(s) for the record. */
84
+ attestation?: DwnMessage[DwnInterface.RecordsWrite]['attestation'];
85
+
86
+ /** The encryption information for the record. */
87
+ encryption?: DwnMessage[DwnInterface.RecordsWrite]['encryption'];
88
+
89
+ /** The contextId associated with the record. */
90
+ contextId?: string;
91
+
92
+ /** The unique identifier of the record */
93
+ recordId?: string;
94
+
95
+ /** The DID of the DWN tenant under which record operations are being performed. */
96
+ connectedDid: string;
97
+
98
+ /** The optional DID that will sign the records on behalf of the connectedDid */
99
+ delegateDid?: string;
100
+
101
+ /** The data of the record, either as a Base64 URL encoded string or a Blob. */
102
+ encodedData?: string | Blob;
103
+
104
+ /**
105
+ * A stream of data, conforming to the `Readable` or `ReadableStream` interface, providing a
106
+ * mechanism to read the record's data sequentially. This is particularly useful for handling
107
+ * large datasets that should not be loaded entirely in memory, allowing for efficient, chunked
108
+ * processing of the record's data.
109
+ */
110
+ data?: Readable | ReadableStream;
111
+
112
+ /** The initial `RecordsWriteMessage` that represents the initial state/version of the record. */
113
+ initialWrite?: DwnMessage[DwnInterface.RecordsWrite];
114
+
115
+ /** The protocol role under which this record is written. */
116
+ protocolRole?: string;
117
+
118
+ /** The remote tenant DID if the record was queried or read from a remote DWN. */
119
+ remoteOrigin?: string;
120
+ };
121
+
122
+ /**
123
+ * Parameters for updating a DWN record.
124
+ *
125
+ * This type specifies the set of properties that can be updated on an existing record. It is used
126
+ * to convey the new state or changes to be applied to the record.
127
+ *
128
+ * @beta
129
+ */
130
+ export type RecordUpdateParams = {
131
+ /**
132
+ * The new data for the record, which can be of any type. This data will replace the existing
133
+ * data of the record. It's essential to ensure that this data is compatible with the record's
134
+ * schema or data format expectations.
135
+ */
136
+ data?: unknown;
137
+
138
+ /**
139
+ * The Content Identifier (CID) of the data. Updating this value changes the reference to the data
140
+ * associated with the record.
141
+ */
142
+ dataCid?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['dataCid'];
143
+
144
+ /** Whether or not to store the updated message. */
145
+ store?: boolean;
146
+
147
+ /** The data format/MIME type of the supplied data */
148
+ dataFormat?: string;
149
+
150
+ /** The size of the data in bytes. */
151
+ dataSize?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['dataSize'];
152
+
153
+ /** The timestamp indicating when the record was last modified. */
154
+ dateModified?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['messageTimestamp'];
155
+
156
+ /** The timestamp indicating when the record was published. */
157
+ datePublished?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['datePublished'];
158
+
159
+ /** The protocol role under which this record is written. */
160
+ protocolRole?: RecordOptions['protocolRole'];
161
+
162
+ /** The published status of the record. */
163
+ published?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['published'];
164
+
165
+
166
+ /** The tags associated with the updated record */
167
+ tags?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['tags'];
168
+ }
169
+
170
+ /**
171
+ * Parameters for deleting a DWN record.
172
+ *
173
+ * This type specifies the set of properties that are used when deleting an existing record. It is used
174
+ * to convey the new state or changes to be applied to the record.
175
+ *
176
+ * @beta
177
+ */
178
+ export type RecordDeleteParams = {
179
+ /** Whether or not to store the message. */
180
+ store?: boolean;
181
+
182
+ /** Whether or not to sign the delete as an owner in order to import it. */
183
+ signAsOwner?: boolean;
184
+
185
+ /** Whether or not to prune any children this record may have. */
186
+ prune?: DwnMessageDescriptor[DwnInterface.RecordsDelete]['prune'];
187
+
188
+ /** The timestamp indicating when the record was deleted. */
189
+ dateModified?: DwnMessageDescriptor[DwnInterface.RecordsDelete]['messageTimestamp'];
190
+
191
+ /** The protocol role under which this record will be deleted. */
192
+ protocolRole?: string;
193
+ };
194
+
195
+ /**
196
+ * The `Record` class encapsulates a single record's data and metadata, providing a more
197
+ * developer-friendly interface for working with Decentralized Web Node (DWN) records.
198
+ *
199
+ * Methods are provided to read, update, and manage the record's lifecycle, including writing to
200
+ * remote DWNs.
201
+ *
202
+ * Note: The `messageTimestamp` of the most recent RecordsWrite message is
203
+ * logically equivalent to the date/time at which a Record was most
204
+ * recently modified. Since this Record class implementation is
205
+ * intended to simplify the developer experience of working with
206
+ * logical records (and not individual DWN messages) the
207
+ * `messageTimestamp` is mapped to `dateModified`.
208
+ *
209
+ * @beta
210
+ */
211
+ export class Record implements RecordModel {
212
+ /**
213
+ * Cache to minimize the amount of redundant two-phase commits we do in store() and send()
214
+ * Retains awareness of the last 100 records stored/sent for up to 100 target DIDs each.
215
+ */
216
+ private static _sendCache = SendCache;
217
+
218
+ // Record instance metadata.
219
+
220
+ /** The {@link Web5Agent} instance that handles DWNs requests. */
221
+ private _agent: Web5Agent;
222
+ /** The DID of the DWN tenant under which operations are being performed. */
223
+ private _connectedDid: string;
224
+ /** The optional DID that is delegated to act on behalf of the connectedDid */
225
+ private _delegateDid?: string;
226
+ /** cache for fetching a permission {@link PermissionGrant}, keyed by a specific MessageType and protocol */
227
+ private _permissionsApi: PermissionsApi;
228
+ /** Encoded data of the record, if available. */
229
+ private _encodedData?: Blob;
230
+ /** Stream of the record's data. */
231
+ private _readableStream?: Readable;
232
+ /** The origin DID if the record was fetched from a remote DWN. */
233
+ private _remoteOrigin?: string;
234
+
235
+ // Private variables for DWN `RecordsWrite` message properties.
236
+
237
+ /** The DID of the entity that most recently authored or deleted the record. */
238
+ private _author: string;
239
+ /** The DID of the entity that originally created the record. */
240
+ private _creator: string;
241
+ /** Attestation JWS signature. */
242
+ private _attestation?: DwnMessage[DwnInterface.RecordsWrite]['attestation'];
243
+ /** Authorization signature(s). */
244
+ private _authorization?: DwnMessage[DwnInterface.RecordsWrite | DwnInterface.RecordsDelete]['authorization'];
245
+ /** Context ID associated with the record. */
246
+ private _contextId?: string;
247
+ /** Descriptor detailing the record's schema, format, and other metadata. */
248
+ private _descriptor: DwnMessageDescriptor[DwnInterface.RecordsWrite] | DwnMessageDescriptor[DwnInterface.RecordsDelete];
249
+ /** Encryption details for the record, if the data is encrypted. */
250
+ private _encryption?: DwnMessage[DwnInterface.RecordsWrite]['encryption'];
251
+ /** Initial state of the record before any updates. */
252
+ private _initialWrite: RecordOptions['initialWrite'];
253
+ /** Flag indicating if the initial write has been stored, to prevent duplicates. */
254
+ private _initialWriteStored: boolean;
255
+ /** Flag indicating if the initial write has been signed by the owner. */
256
+ private _initialWriteSigned: boolean;
257
+ /** Unique identifier of the record. */
258
+ private _recordId: string;
259
+ /** Role under which the record is written. */
260
+ private _protocolRole?: RecordOptions['protocolRole'];
261
+
262
+ /** The `RecordsWriteMessage` descriptor unless the record is in a deleted state */
263
+ private get _recordsWriteDescriptor() {
264
+ if (isDwnMessage(DwnInterface.RecordsWrite, this.rawMessage)) {
265
+ return this._descriptor as DwnMessageDescriptor[DwnInterface.RecordsWrite];
266
+ }
267
+
268
+ return undefined; // returns undefined if the descriptor does not represent a RecordsWrite message.
269
+ }
270
+
271
+ /** The `RecordsWrite` descriptor from the current record or the initial write if the record is in a delete state. */
272
+ private get _immutableProperties(): ImmutableRecordProperties {
273
+ return this._recordsWriteDescriptor || this._initialWrite.descriptor;
274
+ }
275
+
276
+ // Getters for immutable Record properties.
277
+ /** Record's ID */
278
+ get id() { return this._recordId; }
279
+
280
+ /** Record's context ID. If the record is deleted, the context Id comes from the initial write */
281
+ get contextId() { return this.deleted ? this._initialWrite.contextId : this._contextId; }
282
+
283
+ /** Record's creation date */
284
+ get dateCreated() { return this._immutableProperties.dateCreated; }
285
+
286
+ /** Record's parent ID */
287
+ get parentId() { return this._immutableProperties.parentId; }
288
+
289
+ /** Record's protocol */
290
+ get protocol() { return this._immutableProperties.protocol; }
291
+
292
+ /** Record's protocol path */
293
+ get protocolPath() { return this._immutableProperties.protocolPath; }
294
+
295
+ /** Record's recipient */
296
+ get recipient() { return this._immutableProperties.recipient; }
297
+
298
+ /** Record's schema */
299
+ get schema() { return this._immutableProperties.schema; }
300
+
301
+
302
+ // Getters for mutable DWN RecordsWrite properties that may be undefined in a deleted state.
303
+ /** Record's data format */
304
+ get dataFormat() { return this._recordsWriteDescriptor?.dataFormat; }
305
+
306
+ /** Record's CID */
307
+ get dataCid() { return this._recordsWriteDescriptor?.dataCid; }
308
+
309
+ /** Record's data size */
310
+ get dataSize() { return this._recordsWriteDescriptor?.dataSize; }
311
+
312
+ /** Record's published date */
313
+ get datePublished() { return this._recordsWriteDescriptor?.datePublished; }
314
+
315
+ /** Record's published status (true/false) */
316
+ get published() { return this._recordsWriteDescriptor?.published; }
317
+
318
+ /** Tags of the record */
319
+ get tags() { return this._recordsWriteDescriptor?.tags; }
320
+
321
+ // Getters for for properties that depend on the current state of the Record.
322
+ /** DID that is the logical author of the Record. */
323
+ get author(): string { return this._author; }
324
+
325
+ /** DID that is the original creator of the Record. */
326
+ get creator(): string { return this._creator; }
327
+
328
+ /** Record's modified date */
329
+ get dateModified() { return this._descriptor.messageTimestamp; }
330
+
331
+ /** Record's encryption */
332
+ get encryption(): DwnMessage[DwnInterface.RecordsWrite]['encryption'] { return this._encryption; }
333
+
334
+ /** Record's signatures attestation */
335
+ get authorization(): DwnMessage[DwnInterface.RecordsWrite | DwnInterface.RecordsDelete]['authorization'] { return this._authorization; }
336
+
337
+ /** Record's signatures attestation */
338
+ get attestation(): DwnMessage[DwnInterface.RecordsWrite]['attestation'] | undefined { return this._attestation; }
339
+
340
+ /** Role under which the author is writing the record */
341
+ get protocolRole() { return this._protocolRole; }
342
+
343
+ /** Record's deleted state (true/false) */
344
+ get deleted() { return isDwnMessage(DwnInterface.RecordsDelete, this.rawMessage); }
345
+
346
+ /** Record's initial write if the record has been updated */
347
+ get initialWrite(): RecordOptions['initialWrite'] { return this._initialWrite; }
348
+
349
+ /**
350
+ * Returns a copy of the raw `RecordsWriteMessage` that was used to create the current `Record` instance.
351
+ */
352
+ get rawMessage(): DwnMessage[DwnInterface.RecordsWrite] | DwnMessage[DwnInterface.RecordsDelete] {
353
+ const messageType = this._descriptor.interface + this._descriptor.method;
354
+ let message: DwnMessage[DwnInterface.RecordsWrite] | DwnMessage[DwnInterface.RecordsDelete];
355
+ if (messageType === DwnInterface.RecordsWrite) {
356
+ message = JSON.parse(JSON.stringify({
357
+ contextId : this._contextId,
358
+ recordId : this._recordId,
359
+ descriptor : this._descriptor,
360
+ attestation : this._attestation,
361
+ authorization : this._authorization,
362
+ encryption : this._encryption,
363
+ }));
364
+ } else {
365
+ message = JSON.parse(JSON.stringify({
366
+ descriptor : this._descriptor,
367
+ authorization : this._authorization,
368
+ }));
369
+ }
370
+
371
+ removeUndefinedProperties(message);
372
+ return message;
373
+ }
374
+
375
+ constructor(agent: Web5Agent, options: RecordOptions, permissionsApi?: PermissionsApi) {
376
+
377
+ this._agent = agent;
378
+
379
+ // Store the author DID that originally signed the message as a convenience for developers, so
380
+ // that they don't have to decode the signer's DID from the JWS.
381
+ this._author = options.author;
382
+ // The creator is the author of the initial write, or the author of the record if there is no initial write.
383
+ this._creator = options.initialWrite ? getRecordAuthor(options.initialWrite) : options.author;
384
+
385
+ // Store the `connectedDid`, and optionally the `delegateDid` and `permissionsApi` in order to be able
386
+ // to perform operations on the record (update, delete, data) as a delegate of the connected DID.
387
+ this._connectedDid = options.connectedDid;
388
+ this._delegateDid = options.delegateDid;
389
+ this._permissionsApi = permissionsApi ?? new AgentPermissionsApi({ agent });
390
+
391
+ // If the record was queried or read from a remote DWN, the `remoteOrigin` DID will be
392
+ // defined. This value is used to send subsequent read requests to the same remote DWN in the
393
+ // event the record's data payload was too large to be returned in query results. or must be
394
+ // read again (e.g., if the data stream is consumed).
395
+ this._remoteOrigin = options.remoteOrigin;
396
+
397
+ // RecordsWriteMessage properties.
398
+ this._attestation = options.attestation;
399
+ this._authorization = options.authorization;
400
+ this._contextId = options.contextId;
401
+ this._descriptor = options.descriptor;
402
+ this._encryption = options.encryption;
403
+ this._initialWrite = options.initialWrite;
404
+ this._recordId = this.isRecordsDeleteDescriptor(options.descriptor) ? options.descriptor.recordId : options.recordId;
405
+ this._protocolRole = options.protocolRole;
406
+
407
+ if (options.encodedData) {
408
+ // If `encodedData` is set, then it is expected that:
409
+ // type is Blob if the Record object was instantiated by dwn.records.create()/write().
410
+ // type is Base64 URL encoded string if the Record object was instantiated by dwn.records.query().
411
+ // If it is a string, we need to Base64 URL decode to bytes and instantiate a Blob.
412
+ this._encodedData = (typeof options.encodedData === 'string') ?
413
+ new Blob([Convert.base64Url(options.encodedData).toUint8Array()], { type: this.dataFormat }) :
414
+ options.encodedData;
415
+ }
416
+
417
+ if (options.data) {
418
+ // If the record was created from a RecordsRead reply then it will have a `data` property.
419
+ // If the `data` property is a web ReadableStream, convert it to a Node.js Readable.
420
+ this._readableStream = Stream.isReadableStream(options.data) ?
421
+ NodeStream.fromWebReadable({ readableStream: options.data }) :
422
+ options.data;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Returns the data of the current record.
428
+ * If the record data is not available, it attempts to fetch the data from the DWN.
429
+ * @returns a data stream with convenience methods such as `blob()`, `json()`, `text()`, and `stream()`, similar to the fetch API response
430
+ * @throws `Error` if the record has already been deleted.
431
+ *
432
+ * @beta
433
+ */
434
+ get data() {
435
+ const self = this; // Capture the context of the `Record` instance.
436
+ const dataObj = {
437
+
438
+ /**
439
+ * Returns the data of the current record as a `Blob`.
440
+ *
441
+ * @returns A promise that resolves to a Blob containing the record's data.
442
+ * @throws If the record data is not available or cannot be converted to a `Blob`.
443
+ *
444
+ * @beta
445
+ */
446
+ async blob(): Promise<Blob> {
447
+ return new Blob([await NodeStream.consumeToBytes({ readable: await this.stream() })], { type: self.dataFormat });
448
+ },
449
+
450
+ /**
451
+ * Returns the data of the current record as a `Uint8Array`.
452
+ *
453
+ * @returns A Promise that resolves to a `Uint8Array` containing the record's data bytes.
454
+ * @throws If the record data is not available or cannot be converted to a byte array.
455
+ *
456
+ * @beta
457
+ */
458
+ async bytes(): Promise<Uint8Array> {
459
+ return await NodeStream.consumeToBytes({ readable: await this.stream() });
460
+ },
461
+
462
+ /**
463
+ * Parses the data of the current record as JSON and returns it as a JavaScript object.
464
+ *
465
+ * @returns A Promise that resolves to a JavaScript object parsed from the record's JSON data.
466
+ * @throws If the record data is not available, not in JSON format, or cannot be parsed.
467
+ *
468
+ * @beta
469
+ */
470
+ async json(): Promise<any> {
471
+ return await NodeStream.consumeToJson({ readable: await this.stream() });
472
+ },
473
+
474
+ /**
475
+ * Returns the data of the current record as a `string`.
476
+ *
477
+ * @returns A promise that resolves to a `string` containing the record's text data.
478
+ * @throws If the record data is not available or cannot be converted to text.
479
+ *
480
+ * @beta
481
+ */
482
+ async text(): Promise<string> {
483
+ return await NodeStream.consumeToText({ readable: await this.stream() });
484
+ },
485
+
486
+ /**
487
+ * Provides a `Readable` stream containing the record's data.
488
+ *
489
+ * @returns A promise that resolves to a Node.js `Readable` stream of the record's data.
490
+ * @throws If the record data is not available in-memory and cannot be fetched.
491
+ *
492
+ * @beta
493
+ */
494
+ async stream(): Promise<Readable> {
495
+ if (self._encodedData) {
496
+ /** If `encodedData` is set, it indicates that the Record was instantiated by
497
+ * `dwn.records.create()`/`dwn.records.write()` or the record's data payload was small
498
+ * enough to be returned in `dwn.records.query()` results. In either case, the data is
499
+ * already available in-memory and can be returned as a Node.js `Readable` stream. */
500
+ self._readableStream = NodeStream.fromWebReadable({ readableStream: self._encodedData.stream() });
501
+
502
+ } else if (!NodeStream.isReadable({ readable: self._readableStream })) {
503
+ /** If the data stream for this `Record` instance has already been partially or fully
504
+ * consumed, then the data must be fetched again from either: */
505
+ self._readableStream = self._remoteOrigin ?
506
+ // A. ...a remote DWN if the record was originally queried from a remote DWN.
507
+ await self.readRecordData({ target: self._remoteOrigin, isRemote: true }) :
508
+ // B. ...a local DWN if the record was originally queried from the local DWN.
509
+ await self.readRecordData({ target: self._connectedDid, isRemote: false });
510
+ }
511
+
512
+ if (!self._readableStream) {
513
+ throw new Error('Record data is not available.');
514
+ }
515
+
516
+ return self._readableStream;
517
+ },
518
+
519
+ /**
520
+ * Attaches callbacks for the resolution and/or rejection of the `Promise` returned by
521
+ * `stream()`.
522
+ *
523
+ * This method is a proxy to the `then` method of the `Promise` returned by `stream()`,
524
+ * allowing for a seamless integration with promise-based workflows.
525
+ * @param onFulfilled - A function to asynchronously execute when the `stream()` promise
526
+ * becomes fulfilled.
527
+ * @param onRejected - A function to asynchronously execute when the `stream()` promise
528
+ * becomes rejected.
529
+ * @returns A `Promise` for the completion of which ever callback is executed.
530
+ */
531
+ then(onFulfilled?: (value: Readable) => Readable | PromiseLike<Readable>, onRejected?: (reason: any) => PromiseLike<never>) {
532
+ return this.stream().then(onFulfilled, onRejected);
533
+ },
534
+
535
+ /**
536
+ * Attaches a rejection handler callback to the `Promise` returned by the `stream()` method.
537
+ * This method is a shorthand for `.then(undefined, onRejected)`, specifically designed for handling
538
+ * rejection cases in the promise chain initiated by accessing the record's data. It ensures that
539
+ * errors during data retrieval or processing can be caught and handled appropriately.
540
+ *
541
+ * @param onRejected - A function to asynchronously execute when the `stream()` promise
542
+ * becomes rejected.
543
+ * @returns A `Promise` that resolves to the value of the callback if it is called, or to its
544
+ * original fulfillment value if the promise is instead fulfilled.
545
+ */
546
+ catch(onRejected?: (reason: any) => PromiseLike<never>) {
547
+ return this.stream().catch(onRejected);
548
+ }
549
+ };
550
+
551
+ return dataObj;
552
+ }
553
+
554
+ /**
555
+ * Stores the current record state as well as any initial write to the owner's DWN.
556
+ *
557
+ * @param importRecord - if true, the record will signed by the owner before storing it to the owner's DWN. Defaults to false.
558
+ * @returns the status of the store request
559
+ *
560
+ * @beta
561
+ */
562
+ async store(importRecord: boolean = false): Promise<DwnResponseStatus> {
563
+ // if we are importing the record we sign it as the owner
564
+ return this.processRecord({ signAsOwner: importRecord, store: true });
565
+ }
566
+
567
+ /**
568
+ * Signs the current record state as well as any initial write and optionally stores it to the owner's DWN.
569
+ * This is useful when importing a record that was signed by someone else into your own DWN.
570
+ *
571
+ * @param store - if true, the record will be stored to the owner's DWN after signing. Defaults to true.
572
+ * @returns the status of the import request
573
+ *
574
+ * @beta
575
+ */
576
+ async import(store: boolean = true): Promise<DwnResponseStatus> {
577
+ return this.processRecord({ store, signAsOwner: true });
578
+ }
579
+
580
+ /**
581
+ * Send the current record to a remote DWN by specifying their DID
582
+ * If no DID is specified, the target is assumed to be the owner (connectedDID).
583
+ *
584
+ * If an initial write is present and the Record class send cache has no awareness of it, the initial write is sent first
585
+ * (vs waiting for the regular DWN sync)
586
+ *
587
+ * @param target - the optional DID to send the record to, if none is set it is sent to the connectedDid
588
+ * @returns the status of the send record request
589
+ * @throws `Error` if the record has already been deleted.
590
+ *
591
+ * @beta
592
+ */
593
+ async send(target?: string): Promise<DwnResponseStatus> {
594
+ const initialWrite = this._initialWrite;
595
+ target ??= this._connectedDid;
596
+
597
+ // Is there an initial write? Do we know if we've already sent it to this target?
598
+ if (initialWrite && !Record._sendCache.check(this._recordId, target)){
599
+ // We do have an initial write, so prepare it for sending to the target.
600
+ const rawMessage = {
601
+ ...initialWrite
602
+ };
603
+ removeUndefinedProperties(rawMessage);
604
+
605
+ // Send the initial write to the target.
606
+ await this._agent.sendDwnRequest({
607
+ messageType : DwnInterface.RecordsWrite,
608
+ author : this._connectedDid,
609
+ target : target,
610
+ rawMessage
611
+ });
612
+
613
+ // Set the cache to maintain awareness that we don't need to send the initial write next time.
614
+ Record._sendCache.set(this._recordId, target);
615
+ }
616
+
617
+ let sendRequestOptions: SendDwnRequest<DwnInterface.RecordsWrite | DwnInterface.RecordsDelete>;
618
+ if (this.deleted) {
619
+ sendRequestOptions = {
620
+ messageType : DwnInterface.RecordsDelete,
621
+ author : this._connectedDid,
622
+ target : target,
623
+ rawMessage : { ...this.rawMessage }
624
+ };
625
+ } else {
626
+ sendRequestOptions = {
627
+ messageType : DwnInterface.RecordsWrite,
628
+ author : this._connectedDid,
629
+ target : target,
630
+ dataStream : await this.data.blob(),
631
+ rawMessage : { ...this.rawMessage }
632
+ };
633
+ }
634
+
635
+ // Send the current/latest state to the target.
636
+ const { reply } = await this._agent.sendDwnRequest(sendRequestOptions);
637
+ return reply;
638
+ }
639
+
640
+ /**
641
+ * Returns a JSON representation of the Record instance.
642
+ * It's called by `JSON.stringify(...)` automatically.
643
+ */
644
+ toJSON(): RecordModel {
645
+ return {
646
+ attestation : this.attestation,
647
+ author : this.author,
648
+ authorization : this.authorization,
649
+ contextId : this.contextId,
650
+ dataCid : this.dataCid,
651
+ dataFormat : this.dataFormat,
652
+ dataSize : this.dataSize,
653
+ dateCreated : this.dateCreated,
654
+ messageTimestamp : this.dateModified,
655
+ datePublished : this.datePublished,
656
+ encryption : this.encryption,
657
+ parentId : this.parentId,
658
+ protocol : this.protocol,
659
+ protocolPath : this.protocolPath,
660
+ protocolRole : this.protocolRole,
661
+ published : this.published,
662
+ recipient : this.recipient,
663
+ recordId : this.id,
664
+ schema : this.schema,
665
+ tags : this.tags,
666
+ };
667
+ }
668
+
669
+ /**
670
+ * Convenience method to return the string representation of the Record instance.
671
+ * Called automatically in string concatenation, String() type conversion, and template literals.
672
+ */
673
+ toString() {
674
+ let str = `Record: {\n`;
675
+ str += ` ID: ${this.id}\n`;
676
+ str += this.contextId ? ` Context ID: ${this.contextId}\n` : '';
677
+ str += this.protocol ? ` Protocol: ${this.protocol}\n` : '';
678
+ str += this.schema ? ` Schema: ${this.schema}\n` : '';
679
+
680
+ // Only display data properties if the record has not been deleted.
681
+ if (!this.deleted) {
682
+ str += ` Data CID: ${this.dataCid}\n`;
683
+ str += ` Data Format: ${this.dataFormat}\n`;
684
+ str += ` Data Size: ${this.dataSize}\n`;
685
+ }
686
+
687
+ str += ` Deleted: ${this.deleted}\n`;
688
+ str += ` Created: ${this.dateCreated}\n`;
689
+ str += ` Modified: ${this.dateModified}\n`;
690
+ str += `}`;
691
+ return str;
692
+ }
693
+
694
+ /**
695
+ * Returns a pagination cursor for the current record given a sort order.
696
+ *
697
+ * @param sort the sort order to use for the pagination cursor.
698
+ * @returns A promise that resolves to a pagination cursor for the current record.
699
+ */
700
+ async paginationCursor(sort: DwnDateSort): Promise<DwnPaginationCursor | undefined> {
701
+ return isDwnMessage(DwnInterface.RecordsWrite, this.rawMessage) ? getPaginationCursor(this.rawMessage, sort) : undefined;
702
+ }
703
+
704
+ /**
705
+ * Update the current record on the DWN.
706
+ * @param params - Parameters to update the record.
707
+ * @returns the status of the update request
708
+ * @throws `Error` if the record has already been deleted.
709
+ *
710
+ * @beta
711
+ */
712
+ async update({ dateModified, data, protocolRole, store = true, ...params }: RecordUpdateParams): Promise<DwnResponseStatus> {
713
+
714
+ if (this.deleted) {
715
+ throw new Error('Record: Cannot revive a deleted record.');
716
+ }
717
+
718
+ // if there is a parentId, we remove it from the descriptor and set a parentContextId
719
+ const { parentId, ...descriptor } = this._recordsWriteDescriptor;
720
+ const parentContextId = parentId ? this._contextId.split('/').slice(0, -1).join('/') : undefined;
721
+
722
+ // Begin assembling the update message.
723
+ let updateMessage: DwnMessageParams[DwnInterface.RecordsWrite] = {
724
+ ...descriptor,
725
+ ...params,
726
+ parentContextId,
727
+ protocolRole : protocolRole ?? this._protocolRole, // Use the current protocolRole if not provided.
728
+ messageTimestamp : dateModified, // Map Record class `dateModified` property to DWN SDK `messageTimestamp`
729
+ recordId : this._recordId
730
+ };
731
+
732
+ // NOTE: The original Record's tags are copied to the update message, so that the tags are not lost.
733
+ // However if a user passes new tags in the `RecordUpdateParams` object, they will overwrite the original tags.
734
+ // If the updated tag object is empty or set to null, we remove the tags property to avoid schema validation errors in the DWN SDK.
735
+ if (isEmptyObject(updateMessage.tags) || updateMessage.tags === null) {
736
+ delete updateMessage.tags;
737
+ }
738
+
739
+ let dataBlob: Blob;
740
+ if (data !== undefined) {
741
+ // If `data` is being updated then `dataCid` and `dataSize` must be undefined and the `data`
742
+ // value must be converted to a Blob and later passed as a top-level property to
743
+ // `agent.processDwnRequest()`.
744
+ delete updateMessage.dataCid;
745
+ delete updateMessage.dataSize;
746
+ ({ dataBlob } = dataToBlob(data, updateMessage.dataFormat));
747
+ }
748
+
749
+ // Throw an error if an attempt is made to modify immutable properties.
750
+ // Note: `data` and `dateModified` have already been handled.
751
+ const mutableDescriptorProperties = new Set(['data', 'dataCid', 'dataFormat', 'dataSize', 'datePublished', 'messageTimestamp', 'published', 'tags']);
752
+ Record.verifyPermittedMutation(Object.keys(params), mutableDescriptorProperties);
753
+
754
+ // If `published` is set to false, ensure that `datePublished` is undefined. Otherwise, DWN SDK's schema validation
755
+ // will throw an error if `published` is false but `datePublished` is set.
756
+ if (params.published === false && updateMessage.datePublished !== undefined) {
757
+ delete updateMessage.datePublished;
758
+ }
759
+
760
+ const requestOptions: ProcessDwnRequest<DwnInterface.RecordsWrite> = {
761
+ author : this._connectedDid,
762
+ dataStream : dataBlob,
763
+ messageParams : { ...updateMessage },
764
+ messageType : DwnInterface.RecordsWrite,
765
+ target : this._connectedDid,
766
+ store
767
+ };
768
+
769
+ if (this._delegateDid) {
770
+ const { message: delegatedGrant } = await this._permissionsApi.getPermissionForRequest({
771
+ connectedDid : this._connectedDid,
772
+ delegateDid : this._delegateDid,
773
+ protocol : this.protocol,
774
+ delegate : true,
775
+ cached : true,
776
+ messageType : requestOptions.messageType
777
+ });
778
+ requestOptions.messageParams.delegatedGrant = delegatedGrant;
779
+ requestOptions.granteeDid = this._delegateDid;
780
+ }
781
+
782
+ const agentResponse = await this._agent.processDwnRequest(requestOptions);
783
+
784
+ const { message, reply: { status } } = agentResponse;
785
+ const responseMessage = message;
786
+
787
+ if (200 <= status.code && status.code <= 299) {
788
+ // copy the original raw message to the initial write before we update the values.
789
+ if (!this._initialWrite) {
790
+ // If there is no initial write, we need to create one from the current record state.
791
+ // We checked in the beginning of the function that the rawMessage is a RecordsWrite message.
792
+ this._initialWrite = { ...this.rawMessage as DwnMessage[DwnInterface.RecordsWrite] };
793
+ }
794
+
795
+ // Only update the local Record instance mutable properties if the record was successfully (over)written.
796
+ this._authorization = responseMessage.authorization;
797
+ this._protocolRole = updateMessage.protocolRole;
798
+ mutableDescriptorProperties.forEach(property => {
799
+ this._descriptor[property] = responseMessage.descriptor[property];
800
+ });
801
+
802
+ // Cache data.
803
+ if (data !== undefined) {
804
+ this._encodedData = dataBlob;
805
+ }
806
+ }
807
+
808
+ return { status };
809
+ }
810
+
811
+ /**
812
+ * Delete the current record on the DWN.
813
+ * @param params - Parameters to delete the record.
814
+ * @returns the status of the delete request
815
+ */
816
+ async delete(deleteParams?: RecordDeleteParams): Promise<DwnResponseStatus> {
817
+ const { store = true, signAsOwner, dateModified, prune = false } = deleteParams || {};
818
+
819
+ const signAsOwnerValue = signAsOwner && this._delegateDid === undefined;
820
+ const signAsOwnerDelegate = signAsOwner && this._delegateDid !== undefined;
821
+
822
+ if (this.deleted && !this._initialWrite) {
823
+ throw new Error('Record: Record is in an invalid state, initial write is missing.');
824
+ }
825
+
826
+ if (!this._initialWrite) {
827
+ // If there is no initial write, we need to create one from the current record state.
828
+ // We checked in the beginning of the function that the initialWrite is not set if the rawMessage is a RecordsDelete message.
829
+ // So we can safely assume that the rawMessage is a RecordsWrite message.
830
+ this._initialWrite = { ...this.rawMessage as DwnMessage[DwnInterface.RecordsWrite] };
831
+ }
832
+
833
+ await this.processInitialWriteIfNeeded({ store, signAsOwner });
834
+
835
+ // prepare delete options
836
+ let deleteOptions: ProcessDwnRequest<DwnInterface.RecordsDelete> = {
837
+ messageType : DwnInterface.RecordsDelete,
838
+ author : this._connectedDid,
839
+ target : this._connectedDid,
840
+ signAsOwner : signAsOwnerValue,
841
+ signAsOwnerDelegate,
842
+ store
843
+ };
844
+
845
+ // Check to see if the provided protocolRole within the deleteParams is different from the current protocolRole.
846
+ const differentRole = deleteParams?.protocolRole ? getRecordProtocolRole(this.rawMessage) !== deleteParams.protocolRole : false;
847
+ // If the record is already in a deleted state but the protocolRole is different, we need to construct a delete message with the new protocolRole
848
+ // otherwise we can just use the existing delete message.
849
+ if (this.deleted && !differentRole) {
850
+ deleteOptions.rawMessage = this.rawMessage as DwnMessage[DwnInterface.RecordsDelete];
851
+ } else {
852
+ // otherwise we construct a delete message given the `RecordDeleteParams`
853
+ deleteOptions.messageParams = {
854
+ prune : prune,
855
+ recordId : this._recordId,
856
+ messageTimestamp : dateModified,
857
+ protocolRole : deleteParams?.protocolRole ?? this._protocolRole // if no protocolRole is provided, use the current protocolRole
858
+ };
859
+ }
860
+
861
+ if (this._delegateDid) {
862
+ const { message: delegatedGrant } = await this._permissionsApi.getPermissionForRequest({
863
+ connectedDid : this._connectedDid,
864
+ delegateDid : this._delegateDid,
865
+ protocol : this.protocol,
866
+ delegate : true,
867
+ cached : true,
868
+ messageType : deleteOptions.messageType
869
+ });
870
+
871
+ deleteOptions.messageParams = {
872
+ ...deleteOptions.messageParams,
873
+ delegatedGrant
874
+ };
875
+
876
+ deleteOptions.granteeDid = this._delegateDid;
877
+ }
878
+
879
+ const agentResponse = await this._agent.processDwnRequest(deleteOptions);
880
+ const { message, reply: { status } } = agentResponse;
881
+
882
+ if (status.code !== 202) {
883
+ // If the delete was not successful, return the status.
884
+ return { status };
885
+ }
886
+
887
+ // If the delete was successful, update the Record author to the author of the delete message.
888
+ this._author = getRecordAuthor(message);
889
+ this._descriptor = message.descriptor;
890
+ this._authorization = message.authorization;
891
+
892
+ // clear out properties that are not relevant for a deleted record
893
+ this._encodedData = undefined;
894
+ this._encryption = undefined;
895
+ this._attestation = undefined;
896
+ this._contextId = undefined;
897
+
898
+ return { status };
899
+ }
900
+
901
+ /**
902
+ * Process the initial write, if it hasn't already been processed, with the options set for storing and/or signing as the owner.
903
+ */
904
+ private async processInitialWriteIfNeeded({ store, signAsOwner }:{ store: boolean, signAsOwner: boolean }): Promise<void> {
905
+ if (this.initialWrite && ((signAsOwner && !this._initialWriteSigned) || (store && !this._initialWriteStored))) {
906
+ const signAsOwnerValue = signAsOwner && this._delegateDid === undefined;
907
+ const signAsOwnerDelegate = signAsOwner && this._delegateDid !== undefined;
908
+
909
+ const initialWriteRequest: ProcessDwnRequest<DwnInterface.RecordsWrite> = {
910
+ messageType : DwnInterface.RecordsWrite,
911
+ rawMessage : this.initialWrite,
912
+ author : this._connectedDid,
913
+ target : this._connectedDid,
914
+ signAsOwner : signAsOwnerValue,
915
+ signAsOwnerDelegate,
916
+ store,
917
+ };
918
+
919
+ if (this._delegateDid) {
920
+ const { message: delegatedGrant } = await this._permissionsApi.getPermissionForRequest({
921
+ connectedDid : this._connectedDid,
922
+ delegateDid : this._delegateDid,
923
+ protocol : this.protocol,
924
+ delegate : true,
925
+ cached : true,
926
+ messageType : initialWriteRequest.messageType
927
+ });
928
+
929
+ initialWriteRequest.messageParams = {
930
+ ...initialWriteRequest.messageParams,
931
+ delegatedGrant
932
+ };
933
+
934
+ initialWriteRequest.granteeDid = this._delegateDid;
935
+ }
936
+
937
+ // Process the prepared initial write, with the options set for storing and/or signing as the owner.
938
+ const agentResponse = await this._agent.processDwnRequest(initialWriteRequest);
939
+
940
+ const { message, reply: { status } } = agentResponse;
941
+ const responseMessage = message;
942
+
943
+ if (200 <= status.code && status.code <= 299) {
944
+ if (store) this._initialWriteStored = true;
945
+ if (signAsOwner) {
946
+ this._initialWriteSigned = true;
947
+ this.initialWrite.authorization = responseMessage.authorization;
948
+ }
949
+ }
950
+ }
951
+ }
952
+
953
+ /**
954
+ * Handles the various conditions around there being an initial write, whether to store initial/current state,
955
+ * and whether to add an owner signature to the initial write to enable storage when protocol rules require it.
956
+ */
957
+ private async processRecord({ store, signAsOwner }:{ store: boolean, signAsOwner: boolean }): Promise<DwnResponseStatus> {
958
+ const signAsOwnerValue = signAsOwner && this._delegateDid === undefined;
959
+ const signAsOwnerDelegate = signAsOwner && this._delegateDid !== undefined;
960
+
961
+ await this.processInitialWriteIfNeeded({ store, signAsOwner });
962
+
963
+ let requestOptions: ProcessDwnRequest<DwnInterface.RecordsWrite | DwnInterface.RecordsDelete>;
964
+ // Now that we've processed a potential initial write, we can process the current record state.
965
+ // If the record has been deleted, we need to send a delete request. Otherwise, we send a write request.
966
+ if (this.deleted) {
967
+ requestOptions = {
968
+ messageType : DwnInterface.RecordsDelete,
969
+ rawMessage : this.rawMessage,
970
+ author : this._connectedDid,
971
+ target : this._connectedDid,
972
+ signAsOwner : signAsOwnerValue,
973
+ signAsOwnerDelegate,
974
+ store,
975
+ };
976
+ } else {
977
+ requestOptions = {
978
+ messageType : DwnInterface.RecordsWrite,
979
+ rawMessage : this.rawMessage,
980
+ author : this._connectedDid,
981
+ target : this._connectedDid,
982
+ dataStream : await this.data.blob(),
983
+ signAsOwner : signAsOwnerValue,
984
+ signAsOwnerDelegate,
985
+ store,
986
+ };
987
+ }
988
+
989
+ if (this._delegateDid) {
990
+ const { message: delegatedGrant } = await this._permissionsApi.getPermissionForRequest({
991
+ connectedDid : this._connectedDid,
992
+ delegateDid : this._delegateDid,
993
+ protocol : this.protocol,
994
+ delegate : true,
995
+ cached : true,
996
+ messageType : requestOptions.messageType
997
+ });
998
+
999
+ requestOptions.messageParams = {
1000
+ ...requestOptions.messageParams,
1001
+ delegatedGrant
1002
+ };
1003
+
1004
+ requestOptions.granteeDid = this._delegateDid;
1005
+ }
1006
+
1007
+ const agentResponse = await this._agent.processDwnRequest(requestOptions);
1008
+ const { message, reply: { status } } = agentResponse;
1009
+ const responseMessage = message;
1010
+
1011
+ if (200 <= status.code && status.code <= 299) {
1012
+ // If we are signing as the owner, make sure to update the current record state's authorization, because now it will have the owner's signature on it.
1013
+ if (signAsOwner) this._authorization = responseMessage.authorization;
1014
+ }
1015
+
1016
+ return { status };
1017
+ }
1018
+
1019
+ /**
1020
+ * Fetches the record's data from the specified DWN.
1021
+ *
1022
+ * This private method is called when the record data is not available in-memory
1023
+ * and needs to be fetched from either a local or a remote DWN.
1024
+ * It makes a read request to the specified DWN and processes the response to provide
1025
+ * a Node.js `Readable` stream of the record's data.
1026
+ *
1027
+ * @param params - Parameters for fetching the record's data.
1028
+ * @param params.target - The DID of the DWN to fetch the data from.
1029
+ * @param params.isRemote - Indicates whether the target DWN is a remote node.
1030
+ * @returns A Promise that resolves to a Node.js `Readable` stream of the record's data.
1031
+ * @throws If there is an error while fetching or processing the data from the DWN.
1032
+ *
1033
+ * @beta
1034
+ */
1035
+ private async readRecordData({ target, isRemote }: { target: string, isRemote: boolean }) {
1036
+ const readRequest: ProcessDwnRequest<DwnInterface.RecordsRead> = {
1037
+ author : this._connectedDid,
1038
+ messageParams : { filter: { recordId: this.id }, protocolRole: this._protocolRole },
1039
+ messageType : DwnInterface.RecordsRead,
1040
+ target,
1041
+ };
1042
+
1043
+ if (this._delegateDid) {
1044
+ // When reading the data as a delegate, if we don't find a grant we will attempt to read it with the delegate DID as the author.
1045
+ // This allows users to read publicly available data without needing explicit grants.
1046
+ //
1047
+ // NOTE: When a read-only Record class is implemented, callers would have that returned instead when they don't have an explicit permission.
1048
+ // This should fail if a permission is not found, although it should not happen in practice.
1049
+ // TODO: https://github.com/TBD54566975/web5-js/issues/898
1050
+ try {
1051
+ const { message: delegatedGrant } = await this._permissionsApi.getPermissionForRequest({
1052
+ connectedDid : this._connectedDid,
1053
+ delegateDid : this._delegateDid,
1054
+ protocol : this.protocol,
1055
+ delegate : true,
1056
+ cached : true,
1057
+ messageType : readRequest.messageType
1058
+ });
1059
+
1060
+ readRequest.messageParams = {
1061
+ ...readRequest.messageParams,
1062
+ delegatedGrant
1063
+ };
1064
+
1065
+ readRequest.granteeDid = this._delegateDid;
1066
+ } catch(error) {
1067
+ // If there is an error fetching the grant, we will attempt to read the data as the delegate.
1068
+ readRequest.author = this._delegateDid;
1069
+ }
1070
+ }
1071
+
1072
+ const agentResponsePromise = isRemote ?
1073
+ this._agent.sendDwnRequest(readRequest) :
1074
+ this._agent.processDwnRequest(readRequest);
1075
+
1076
+ try {
1077
+ const { reply: { status, entry }} = await agentResponsePromise;
1078
+ if (status.code !== 200) {
1079
+ throw new Error(`${status.code}: ${status.detail}`);
1080
+ }
1081
+
1082
+ const dataStream: ReadableStream | Readable = entry.data;
1083
+ // If the data stream is a web ReadableStream, convert it to a Node.js Readable.
1084
+ const nodeReadable = Stream.isReadableStream(dataStream) ?
1085
+ NodeStream.fromWebReadable({ readableStream: dataStream }) :
1086
+ dataStream;
1087
+ return nodeReadable;
1088
+
1089
+ } catch (error) {
1090
+ throw new Error(`Error encountered while attempting to read data: ${error.message}`);
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * Verifies if the properties to be mutated are mutable.
1096
+ *
1097
+ * This private method is used to ensure that only mutable properties of the `Record` instance
1098
+ * are being changed. It checks whether the properties specified for mutation are among the
1099
+ * set of properties that are allowed to be modified. If any of the properties to be mutated
1100
+ * are not in the set of mutable properties, the method throws an error.
1101
+ *
1102
+ * @param propertiesToMutate - An iterable of property names that are intended to be mutated.
1103
+ * @param mutableDescriptorProperties - A set of property names that are allowed to be mutated.
1104
+ *
1105
+ * @throws If any of the properties in `propertiesToMutate` are not in `mutableDescriptorProperties`.
1106
+ *
1107
+ * @beta
1108
+ */
1109
+ private static verifyPermittedMutation(propertiesToMutate: Iterable<string>, mutableDescriptorProperties: Set<string>) {
1110
+ for (const property of propertiesToMutate) {
1111
+ if (!mutableDescriptorProperties.has(property)) {
1112
+ throw new Error(`${property} is an immutable property. Its value cannot be changed.`);
1113
+ }
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * Checks if the descriptor is a RecordsDelete descriptor.
1119
+ *
1120
+ * @param descriptor a RecordsWrite or RecordsDelete descriptor
1121
+ */
1122
+ private isRecordsDeleteDescriptor(descriptor: DwnMessageDescriptor[DwnInterface.RecordsWrite | DwnInterface.RecordsDelete]): descriptor is DwnMessageDescriptor[DwnInterface.RecordsDelete] {
1123
+ return descriptor.interface + descriptor.method === DwnInterface.RecordsDelete;
1124
+ }
1125
+ }