@enbox/api 0.1.1 → 0.2.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 (58) hide show
  1. package/README.md +505 -138
  2. package/dist/browser.mjs +23 -13
  3. package/dist/browser.mjs.map +4 -4
  4. package/dist/esm/advanced.js +11 -0
  5. package/dist/esm/advanced.js.map +1 -0
  6. package/dist/esm/define-protocol.js +3 -3
  7. package/dist/esm/dwn-api.js +56 -114
  8. package/dist/esm/dwn-api.js.map +1 -1
  9. package/dist/esm/dwn-reader-api.js +128 -0
  10. package/dist/esm/dwn-reader-api.js.map +1 -0
  11. package/dist/esm/index.js +3 -2
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/live-query.js +5 -4
  14. package/dist/esm/live-query.js.map +1 -1
  15. package/dist/esm/read-only-record.js +255 -0
  16. package/dist/esm/read-only-record.js.map +1 -0
  17. package/dist/esm/record.js +46 -57
  18. package/dist/esm/record.js.map +1 -1
  19. package/dist/esm/typed-web5.js +242 -0
  20. package/dist/esm/typed-web5.js.map +1 -0
  21. package/dist/esm/web5.js +71 -3
  22. package/dist/esm/web5.js.map +1 -1
  23. package/dist/types/advanced.d.ts +12 -0
  24. package/dist/types/advanced.d.ts.map +1 -0
  25. package/dist/types/define-protocol.d.ts +3 -3
  26. package/dist/types/dwn-api.d.ts +13 -92
  27. package/dist/types/dwn-api.d.ts.map +1 -1
  28. package/dist/types/dwn-reader-api.d.ts +138 -0
  29. package/dist/types/dwn-reader-api.d.ts.map +1 -0
  30. package/dist/types/index.d.ts +3 -2
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/dist/types/live-query.d.ts +13 -1
  33. package/dist/types/live-query.d.ts.map +1 -1
  34. package/dist/types/protocol-types.d.ts +2 -2
  35. package/dist/types/read-only-record.d.ts +133 -0
  36. package/dist/types/read-only-record.d.ts.map +1 -0
  37. package/dist/types/record.d.ts +37 -17
  38. package/dist/types/record.d.ts.map +1 -1
  39. package/dist/types/{typed-dwn-api.d.ts → typed-web5.d.ts} +70 -73
  40. package/dist/types/typed-web5.d.ts.map +1 -0
  41. package/dist/types/web5.d.ts +79 -3
  42. package/dist/types/web5.d.ts.map +1 -1
  43. package/package.json +10 -6
  44. package/src/advanced.ts +29 -0
  45. package/src/define-protocol.ts +3 -3
  46. package/src/dwn-api.ts +91 -232
  47. package/src/dwn-reader-api.ts +255 -0
  48. package/src/index.ts +3 -2
  49. package/src/live-query.ts +20 -4
  50. package/src/protocol-types.ts +2 -2
  51. package/src/read-only-record.ts +328 -0
  52. package/src/record.ts +116 -86
  53. package/src/typed-web5.ts +445 -0
  54. package/src/web5.ts +104 -5
  55. package/dist/esm/typed-dwn-api.js +0 -182
  56. package/dist/esm/typed-dwn-api.js.map +0 -1
  57. package/dist/types/typed-dwn-api.d.ts.map +0 -1
  58. package/src/typed-dwn-api.ts +0 -370
@@ -0,0 +1,255 @@
1
+ /**
2
+ * NOTE: Added reference types here to avoid a `pnpm` bug during build.
3
+ * https://github.com/enboxorg/enbox/pull/507
4
+ */
5
+ /// <reference types="@enbox/dwn-sdk-js" />
6
+
7
+ import type { AnonymousDwnApi, DwnPaginationCursor, DwnResponseStatus } from '@enbox/agent';
8
+ import type {
9
+ DateSort,
10
+ Pagination,
11
+ ProtocolDefinition,
12
+ ProtocolsQueryFilter,
13
+ RecordsFilter,
14
+ } from '@enbox/dwn-sdk-js';
15
+
16
+ import { ReadOnlyRecord } from './read-only-record.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Request / Response types for records
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Request to query public records from a remote DWN.
24
+ *
25
+ * @beta
26
+ */
27
+ export type ReaderRecordsQueryRequest = {
28
+ /** The DID of the remote DWN to query (required — reader is remote-only). */
29
+ from: string;
30
+ /** Filter criteria for the query. */
31
+ filter: RecordsFilter;
32
+ /** Sort order for results. */
33
+ dateSort?: DateSort;
34
+ /** Pagination options. */
35
+ pagination?: Pagination;
36
+ };
37
+
38
+ /**
39
+ * Response from a reader records query.
40
+ *
41
+ * @beta
42
+ */
43
+ export type ReaderRecordsQueryResponse = DwnResponseStatus & {
44
+ /** Array of read-only records matching the query. */
45
+ records: ReadOnlyRecord[];
46
+ /** Pagination cursor for fetching the next page. */
47
+ cursor?: DwnPaginationCursor;
48
+ };
49
+
50
+ /**
51
+ * Request to read a specific public record from a remote DWN.
52
+ *
53
+ * @beta
54
+ */
55
+ export type ReaderRecordsReadRequest = {
56
+ /** The DID of the remote DWN to read from (required — reader is remote-only). */
57
+ from: string;
58
+ /** Filter to identify the record (typically `{ recordId: '...' }`). */
59
+ filter: RecordsFilter;
60
+ };
61
+
62
+ /**
63
+ * Response from a reader records read.
64
+ *
65
+ * @beta
66
+ */
67
+ export type ReaderRecordsReadResponse = DwnResponseStatus & {
68
+ /** The read-only record, if found. */
69
+ record?: ReadOnlyRecord;
70
+ };
71
+
72
+ /**
73
+ * Request to count public records on a remote DWN.
74
+ *
75
+ * @beta
76
+ */
77
+ export type ReaderRecordsCountRequest = {
78
+ /** The DID of the remote DWN to count records in (required). */
79
+ from: string;
80
+ /** Filter criteria for counting. */
81
+ filter: RecordsFilter;
82
+ };
83
+
84
+ /**
85
+ * Response from a reader records count.
86
+ *
87
+ * @beta
88
+ */
89
+ export type ReaderRecordsCountResponse = DwnResponseStatus & {
90
+ /** The number of matching public records. */
91
+ count?: number;
92
+ };
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Request / Response types for protocols
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Request to query published protocols from a remote DWN.
100
+ *
101
+ * @beta
102
+ */
103
+ export type ReaderProtocolsQueryRequest = {
104
+ /** The DID of the remote DWN to query protocols from (required). */
105
+ from: string;
106
+ /** Optional filter for the protocol query. */
107
+ filter?: ProtocolsQueryFilter;
108
+ };
109
+
110
+ /**
111
+ * Response from a reader protocols query.
112
+ *
113
+ * @beta
114
+ */
115
+ export type ReaderProtocolsQueryResponse = DwnResponseStatus & {
116
+ /** Array of published protocol definitions. */
117
+ protocols: ProtocolDefinition[];
118
+ };
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // DwnReaderApi
122
+ // ---------------------------------------------------------------------------
123
+
124
+ /**
125
+ * A read-only API for querying public data on remote DWNs without any identity or signing keys.
126
+ *
127
+ * This class mirrors the shape of {@link DwnApi}'s `records` and `protocols`
128
+ * namespaces, but restricts to read-path operations and requires a `from` DID
129
+ * on every call (remote-only). All messages are unsigned, so only published
130
+ * records and protocols are accessible.
131
+ *
132
+ * Obtain an instance via {@link Web5.anonymous | `Web5.anonymous()`}.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const { dwn } = Web5.anonymous();
137
+ *
138
+ * const { records } = await dwn.records.query({
139
+ * from: 'did:dht:alice...',
140
+ * filter: { protocol: 'https://social.example/posts', protocolPath: 'post' },
141
+ * });
142
+ *
143
+ * for (const record of records) {
144
+ * console.log(record.id, await record.data.text());
145
+ * }
146
+ * ```
147
+ *
148
+ * @beta
149
+ */
150
+ export class DwnReaderApi {
151
+ private _anonymousDwn: AnonymousDwnApi;
152
+
153
+ constructor(anonymousDwn: AnonymousDwnApi) {
154
+ this._anonymousDwn = anonymousDwn;
155
+ }
156
+
157
+ /**
158
+ * API to interact with public DWN records (query, read, count).
159
+ */
160
+ get records(): {
161
+ query: (request: ReaderRecordsQueryRequest) => Promise<ReaderRecordsQueryResponse>;
162
+ read: (request: ReaderRecordsReadRequest) => Promise<ReaderRecordsReadResponse>;
163
+ count: (request: ReaderRecordsCountRequest) => Promise<ReaderRecordsCountResponse>;
164
+ } {
165
+ return {
166
+ /**
167
+ * Query public records from a remote DWN.
168
+ * Only published records are returned.
169
+ */
170
+ query: async (request: ReaderRecordsQueryRequest): Promise<ReaderRecordsQueryResponse> => {
171
+ const reply = await this._anonymousDwn.recordsQuery(request.from, {
172
+ filter : request.filter,
173
+ dateSort : request.dateSort,
174
+ pagination : request.pagination,
175
+ });
176
+
177
+ const { entries = [], status, cursor } = reply;
178
+
179
+ const records = entries.map((entry) => new ReadOnlyRecord({
180
+ rawMessage : entry,
181
+ initialWrite : entry.initialWrite,
182
+ encodedData : entry.encodedData,
183
+ remoteOrigin : request.from,
184
+ anonymousDwn : this._anonymousDwn,
185
+ }));
186
+
187
+ return { records, status, cursor };
188
+ },
189
+
190
+ /**
191
+ * Read a specific public record from a remote DWN.
192
+ * Succeeds for published records and protocol records with `{ who: 'anyone', can: ['read'] }`.
193
+ */
194
+ read: async (request: ReaderRecordsReadRequest): Promise<ReaderRecordsReadResponse> => {
195
+ const reply = await this._anonymousDwn.recordsRead(request.from, {
196
+ filter: request.filter,
197
+ });
198
+
199
+ const { entry, status } = reply;
200
+
201
+ let record: ReadOnlyRecord | undefined;
202
+ if (200 <= status.code && status.code <= 299 && entry?.recordsWrite) {
203
+ record = new ReadOnlyRecord({
204
+ rawMessage : entry.recordsWrite,
205
+ initialWrite : entry.initialWrite,
206
+ data : entry.data,
207
+ remoteOrigin : request.from,
208
+ anonymousDwn : this._anonymousDwn,
209
+ });
210
+ }
211
+
212
+ return { record, status };
213
+ },
214
+
215
+ /**
216
+ * Count public records on a remote DWN.
217
+ * Only published records are counted.
218
+ */
219
+ count: async (request: ReaderRecordsCountRequest): Promise<ReaderRecordsCountResponse> => {
220
+ const reply = await this._anonymousDwn.recordsCount(request.from, {
221
+ filter: request.filter,
222
+ });
223
+
224
+ const { count, status } = reply;
225
+
226
+ return { count, status };
227
+ },
228
+ };
229
+ }
230
+
231
+ /**
232
+ * API to query published protocol definitions from remote DWNs.
233
+ */
234
+ get protocols(): {
235
+ query: (request: ReaderProtocolsQueryRequest) => Promise<ReaderProtocolsQueryResponse>;
236
+ } {
237
+ return {
238
+ /**
239
+ * Query published protocols from a remote DWN.
240
+ * Only protocol definitions with `published: true` are returned.
241
+ */
242
+ query: async (request: ReaderProtocolsQueryRequest): Promise<ReaderProtocolsQueryResponse> => {
243
+ const reply = await this._anonymousDwn.protocolsQuery(request.from, {
244
+ filter: request.filter,
245
+ });
246
+
247
+ const { entries = [], status } = reply;
248
+
249
+ const protocols = entries.map((entry) => entry.descriptor.definition);
250
+
251
+ return { protocols, status };
252
+ },
253
+ };
254
+ }
255
+ }
package/src/index.ts CHANGED
@@ -23,15 +23,16 @@
23
23
 
24
24
  export * from './define-protocol.js';
25
25
  export * from './did-api.js';
26
- export * from './dwn-api.js';
26
+ export * from './dwn-reader-api.js';
27
27
  export * from './grant-revocation.js';
28
28
  export * from './live-query.js';
29
29
  export * from './permission-grant.js';
30
30
  export * from './permission-request.js';
31
31
  export * from './protocol.js';
32
32
  export * from './protocol-types.js';
33
+ export * from './read-only-record.js';
33
34
  export * from './record.js';
34
- export * from './typed-dwn-api.js';
35
+ export * from './typed-web5.js';
35
36
  export * from './vc-api.js';
36
37
  export * from './web5.js';
37
38
 
package/src/live-query.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { RecordsQueryReplyEntry } from '@enbox/dwn-sdk-js';
2
1
  import type { DwnMessageSubscription, PermissionsApi, Web5Agent } from '@enbox/agent';
2
+ import type { PaginationCursor, RecordsQueryReplyEntry } from '@enbox/dwn-sdk-js';
3
3
 
4
4
  import { getRecordAuthor } from '@enbox/agent';
5
5
 
@@ -60,6 +60,9 @@ export type LiveQueryOptions = {
60
60
  /** The initial snapshot entries from the subscribe reply. */
61
61
  initialEntries: RecordsQueryReplyEntry[];
62
62
 
63
+ /** Pagination cursor for fetching the next page of initial results. */
64
+ cursor?: PaginationCursor;
65
+
63
66
  /** The underlying DWN subscription handle. */
64
67
  subscription: DwnMessageSubscription;
65
68
  };
@@ -116,6 +119,17 @@ export class LiveQuery extends EventTarget {
116
119
  /** The initial snapshot of matching records. */
117
120
  readonly records: Record[];
118
121
 
122
+ /**
123
+ * Pagination cursor for fetching the next page of initial results.
124
+ *
125
+ * When the initial snapshot was limited (via `pagination.limit`), this
126
+ * cursor can be passed in a subsequent `records.subscribe()` call to
127
+ * continue from where the previous snapshot left off.
128
+ *
129
+ * `undefined` when there are no more results.
130
+ */
131
+ readonly cursor?: PaginationCursor;
132
+
119
133
  /** The underlying DWN subscription handle. */
120
134
  private _subscription: DwnMessageSubscription;
121
135
 
@@ -131,6 +145,7 @@ export class LiveQuery extends EventTarget {
131
145
  const {
132
146
  agent,
133
147
  connectedDid,
148
+ cursor,
134
149
  delegateDid,
135
150
  protocolRole,
136
151
  remoteOrigin,
@@ -140,6 +155,7 @@ export class LiveQuery extends EventTarget {
140
155
  } = options;
141
156
 
142
157
  this._subscription = subscription;
158
+ this.cursor = cursor;
143
159
 
144
160
  // Build Record objects from the initial snapshot entries (same logic as records.query()).
145
161
  this.records = initialEntries.map((entry) => new Record(agent, {
@@ -154,7 +170,7 @@ export class LiveQuery extends EventTarget {
154
170
  // Seed the known-records map with recordId -> messageTimestamp for dedup.
155
171
  this._knownRecords = new Map();
156
172
  for (const record of this.records) {
157
- this._knownRecords.set(record.id, record.dateModified);
173
+ this._knownRecords.set(record.id, record.timestamp);
158
174
  }
159
175
  }
160
176
 
@@ -179,7 +195,7 @@ export class LiveQuery extends EventTarget {
179
195
 
180
196
  if (knownTimestamp !== undefined) {
181
197
  // We've seen this recordId before (either from snapshot or a prior event).
182
- if (record.dateModified <= knownTimestamp) {
198
+ if (record.timestamp <= knownTimestamp) {
183
199
  // Duplicate or stale event from the overlap window — skip.
184
200
  return;
185
201
  }
@@ -189,7 +205,7 @@ export class LiveQuery extends EventTarget {
189
205
  }
190
206
 
191
207
  // Update the known state.
192
- this._knownRecords.set(record.id, record.dateModified);
208
+ this._knownRecords.set(record.id, record.timestamp);
193
209
  }
194
210
 
195
211
  const change: RecordChange = { type: changeType, record };
@@ -133,7 +133,7 @@ export type TagKeys<Tags extends ProtocolTagsDefinition> = Exclude<
133
133
  /**
134
134
  * A mapping from protocol type names to their TypeScript data shapes.
135
135
  *
136
- * Used as a type parameter to `defineProtocol()` and `TypedDwnApi` so that
136
+ * Used as a type parameter to `defineProtocol()` and `TypedWeb5` so that
137
137
  * the protocol definition JSON stays JSON-compatible while TypeScript types
138
138
  * are tracked separately.
139
139
  *
@@ -154,7 +154,7 @@ export type SchemaMap = Record<string, unknown>;
154
154
  /**
155
155
  * The return type of `defineProtocol()`. Bundles the raw protocol definition
156
156
  * with its inferred path strings and schema type map for downstream use
157
- * by `TypedDwnApi`.
157
+ * by `TypedWeb5`.
158
158
  */
159
159
  export type TypedProtocol<
160
160
  D extends ProtocolDefinition = ProtocolDefinition,
@@ -0,0 +1,328 @@
1
+ /**
2
+ * NOTE: Added reference types here to avoid a `pnpm` bug during build.
3
+ * https://github.com/enboxorg/enbox/pull/507
4
+ */
5
+ /// <reference types="@enbox/dwn-sdk-js" />
6
+
7
+ import type { AnonymousDwnApi } from '@enbox/agent';
8
+ import type { RecordsWriteDescriptor, RecordsWriteMessage, RecordsWriteTags } from '@enbox/dwn-sdk-js';
9
+
10
+ import { getRecordAuthor } from '@enbox/agent';
11
+ import { Convert, Stream } from '@enbox/common';
12
+
13
+ /**
14
+ * Construction options for a {@link ReadOnlyRecord}.
15
+ *
16
+ * @beta
17
+ */
18
+ export type ReadOnlyRecordOptions = {
19
+ /** The raw `RecordsWriteMessage` returned from a query or read reply. */
20
+ rawMessage: RecordsWriteMessage;
21
+ /** The initial write message, if the record has been updated. */
22
+ initialWrite?: RecordsWriteMessage;
23
+ /** Encoded data (Base64URL string) if the data was small enough to be inlined in the query reply. */
24
+ encodedData?: string;
25
+ /** A readable data stream, present when the record comes from a `RecordsRead` reply. */
26
+ data?: ReadableStream;
27
+ /** The DID of the remote DWN this record was fetched from. Used for data re-fetch. */
28
+ remoteOrigin: string;
29
+ /** The {@link AnonymousDwnApi} instance used to re-fetch data when needed. */
30
+ anonymousDwn: AnonymousDwnApi;
31
+ };
32
+
33
+ /**
34
+ * An immutable, read-only view of a DWN record.
35
+ *
36
+ * `ReadOnlyRecord` is returned by {@link DwnReaderApi} methods and provides
37
+ * access to the record's metadata and data without any mutation capabilities.
38
+ * There are no `update()`, `delete()`, `send()`, `store()`, or `import()`
39
+ * methods — the compiler prevents accidental writes.
40
+ *
41
+ * Data access works identically to the full {@link Record} class:
42
+ * - If the data was inlined (small payloads from query replies), it is
43
+ * available immediately.
44
+ * - If the data was not inlined, `data.stream()` / `data.text()` / etc.
45
+ * automatically perform an anonymous `RecordsRead` to fetch it.
46
+ *
47
+ * @beta
48
+ */
49
+ export class ReadOnlyRecord {
50
+ // Private backing fields.
51
+ private _anonymousDwn: AnonymousDwnApi;
52
+ private _remoteOrigin: string;
53
+ private _author: string;
54
+ private _creator: string;
55
+ private _descriptor: RecordsWriteDescriptor;
56
+ private _recordId: string;
57
+ private _contextId?: string;
58
+ private _initialWrite?: RecordsWriteMessage;
59
+ private _encodedData?: Blob;
60
+ private _readableStream?: ReadableStream;
61
+ private _authorization: RecordsWriteMessage['authorization'];
62
+ private _attestation?: RecordsWriteMessage['attestation'];
63
+ private _encryption?: RecordsWriteMessage['encryption'];
64
+
65
+ constructor(options: ReadOnlyRecordOptions) {
66
+ const { rawMessage, initialWrite, encodedData, data, remoteOrigin, anonymousDwn } = options;
67
+
68
+ this._anonymousDwn = anonymousDwn;
69
+ this._remoteOrigin = remoteOrigin;
70
+
71
+ // Extract the author DID from the authorization signature. The author is
72
+ // the DID that signed the most recent RecordsWrite message. For records
73
+ // returned from anonymous queries, the authorization will be present (the
74
+ // remote DWN always returns the full message), but we guard against
75
+ // malformed messages gracefully.
76
+ try {
77
+ this._author = getRecordAuthor(rawMessage) ?? 'unknown';
78
+ } catch {
79
+ this._author = 'unknown';
80
+ }
81
+ try {
82
+ this._creator = initialWrite ? (getRecordAuthor(initialWrite) ?? this._author) : this._author;
83
+ } catch {
84
+ this._creator = this._author;
85
+ }
86
+ this._descriptor = rawMessage.descriptor;
87
+ this._recordId = rawMessage.recordId;
88
+ this._contextId = rawMessage.contextId;
89
+ this._initialWrite = initialWrite;
90
+ this._authorization = rawMessage.authorization;
91
+ this._attestation = rawMessage.attestation;
92
+ this._encryption = rawMessage.encryption;
93
+
94
+ if (encodedData) {
95
+ this._encodedData = new Blob(
96
+ [Convert.base64Url(encodedData).toUint8Array()],
97
+ { type: this.dataFormat },
98
+ );
99
+ }
100
+
101
+ if (data) {
102
+ this._readableStream = data;
103
+ }
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Immutable record properties
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /** Record's unique identifier. */
111
+ get id(): string { return this._recordId; }
112
+
113
+ /** Record's context ID. */
114
+ get contextId(): string | undefined { return this._contextId; }
115
+
116
+ /** Record's creation date. */
117
+ get dateCreated(): string { return this._descriptor.dateCreated; }
118
+
119
+ /** Record's parent ID. */
120
+ get parentId(): string | undefined { return this._descriptor.parentId; }
121
+
122
+ /** Record's protocol URI. */
123
+ get protocol(): string | undefined { return this._descriptor.protocol; }
124
+
125
+ /** Record's protocol path. */
126
+ get protocolPath(): string | undefined { return this._descriptor.protocolPath; }
127
+
128
+ /** Record's recipient. */
129
+ get recipient(): string | undefined { return this._descriptor.recipient; }
130
+
131
+ /** Record's schema. */
132
+ get schema(): string | undefined { return this._descriptor.schema; }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Mutable descriptor properties
136
+ // ---------------------------------------------------------------------------
137
+
138
+ /** Record's data format / MIME type. */
139
+ get dataFormat(): string { return this._descriptor.dataFormat; }
140
+
141
+ /** Record's data CID. */
142
+ get dataCid(): string { return this._descriptor.dataCid; }
143
+
144
+ /** Record's data size in bytes. */
145
+ get dataSize(): number { return this._descriptor.dataSize; }
146
+
147
+ /** Record's published date. */
148
+ get datePublished(): string | undefined { return this._descriptor.datePublished; }
149
+
150
+ /** Whether the record is published. */
151
+ get published(): boolean | undefined { return this._descriptor.published; }
152
+
153
+ /** Tags associated with the record. */
154
+ get tags(): RecordsWriteTags | undefined { return this._descriptor.tags; }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // State-dependent properties
158
+ // ---------------------------------------------------------------------------
159
+
160
+ /** DID that is the logical author of the record. */
161
+ get author(): string { return this._author; }
162
+
163
+ /** DID that originally created the record. */
164
+ get creator(): string { return this._creator; }
165
+
166
+ /** Record's message timestamp (time of most recent create/update). */
167
+ get timestamp(): string { return this._descriptor.messageTimestamp; }
168
+
169
+ /** Record's encryption metadata, if encrypted. */
170
+ get encryption(): RecordsWriteMessage['encryption'] { return this._encryption; }
171
+
172
+ /** Record's authorization. */
173
+ get authorization(): RecordsWriteMessage['authorization'] { return this._authorization; }
174
+
175
+ /** Record's attestation signatures. */
176
+ get attestation(): RecordsWriteMessage['attestation'] { return this._attestation; }
177
+
178
+ /** The initial write message, if the record has been updated. */
179
+ get initialWrite(): RecordsWriteMessage | undefined { return this._initialWrite; }
180
+
181
+ /** The DID of the remote DWN this record was fetched from. */
182
+ get remoteOrigin(): string { return this._remoteOrigin; }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Data access
186
+ // ---------------------------------------------------------------------------
187
+
188
+ /**
189
+ * Returns the data of the current record.
190
+ * If the data is not available in-memory, it is fetched from the remote DWN
191
+ * using an anonymous `RecordsRead`.
192
+ *
193
+ * @returns A data accessor with `blob()`, `bytes()`, `json()`, `text()`, and `stream()` methods.
194
+ *
195
+ * @beta
196
+ */
197
+ get data(): {
198
+ blob: () => Promise<Blob>;
199
+ bytes: () => Promise<Uint8Array>;
200
+ json: <T = unknown>() => Promise<T>;
201
+ text: () => Promise<string>;
202
+ stream: () => Promise<ReadableStream>;
203
+ then: (
204
+ onFulfilled?: (value: ReadableStream) => ReadableStream | PromiseLike<ReadableStream>,
205
+ onRejected?: (reason: any) => PromiseLike<never>,
206
+ ) => Promise<ReadableStream>;
207
+ catch: (onRejected?: (reason: any) => PromiseLike<never>) => Promise<ReadableStream>;
208
+ } {
209
+ const self = this;
210
+ const dataObj = {
211
+ async blob(): Promise<Blob> {
212
+ return new Blob([await Stream.consumeToBytes({ readableStream: await this.stream() })], { type: self.dataFormat });
213
+ },
214
+
215
+ async bytes(): Promise<Uint8Array> {
216
+ return await Stream.consumeToBytes({ readableStream: await this.stream() });
217
+ },
218
+
219
+ async json<T = unknown>(): Promise<T> {
220
+ return await Stream.consumeToJson({ readableStream: await this.stream() }) as T;
221
+ },
222
+
223
+ async text(): Promise<string> {
224
+ return await Stream.consumeToText({ readableStream: await this.stream() });
225
+ },
226
+
227
+ async stream(): Promise<ReadableStream> {
228
+ if (self._encodedData) {
229
+ return Stream.fromBlob(self._encodedData);
230
+ } else if (self._readableStream) {
231
+ const currentStream = self._readableStream;
232
+ self._readableStream = undefined;
233
+ return currentStream;
234
+ } else {
235
+ // Re-fetch the data from the remote DWN using an anonymous RecordsRead.
236
+ return await self.readRecordData();
237
+ }
238
+ },
239
+
240
+ then(
241
+ onFulfilled?: (value: ReadableStream) => ReadableStream | PromiseLike<ReadableStream>,
242
+ onRejected?: (reason: any) => PromiseLike<never>,
243
+ ): Promise<ReadableStream> {
244
+ return this.stream().then(onFulfilled, onRejected);
245
+ },
246
+
247
+ catch(onRejected?: (reason: any) => PromiseLike<never>): Promise<ReadableStream> {
248
+ return this.stream().catch(onRejected);
249
+ },
250
+ };
251
+
252
+ return dataObj;
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Serialization
257
+ // ---------------------------------------------------------------------------
258
+
259
+ /**
260
+ * Returns a JSON representation of the record.
261
+ * Called by `JSON.stringify(...)` automatically.
262
+ */
263
+ toJSON(): Record<string, unknown> {
264
+ return {
265
+ attestation : this.attestation,
266
+ author : this.author,
267
+ authorization : this.authorization,
268
+ contextId : this.contextId,
269
+ dataCid : this.dataCid,
270
+ dataFormat : this.dataFormat,
271
+ dataSize : this.dataSize,
272
+ dateCreated : this.dateCreated,
273
+ datePublished : this.datePublished,
274
+ encryption : this.encryption,
275
+ parentId : this.parentId,
276
+ protocol : this.protocol,
277
+ protocolPath : this.protocolPath,
278
+ published : this.published,
279
+ recipient : this.recipient,
280
+ recordId : this.id,
281
+ schema : this.schema,
282
+ tags : this.tags,
283
+ timestamp : this.timestamp,
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Convenience string representation.
289
+ */
290
+ toString(): string {
291
+ let str = 'ReadOnlyRecord: {\n';
292
+ str += ` ID: ${this.id}\n`;
293
+ str += this.contextId ? ` Context ID: ${this.contextId}\n` : '';
294
+ str += this.protocol ? ` Protocol: ${this.protocol}\n` : '';
295
+ str += this.schema ? ` Schema: ${this.schema}\n` : '';
296
+ str += ` Data CID: ${this.dataCid}\n`;
297
+ str += ` Data Format: ${this.dataFormat}\n`;
298
+ str += ` Data Size: ${this.dataSize}\n`;
299
+ str += ` Created: ${this.dateCreated}\n`;
300
+ str += ` Timestamp: ${this.timestamp}\n`;
301
+ str += '}';
302
+ return str;
303
+ }
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // Private helpers
307
+ // ---------------------------------------------------------------------------
308
+
309
+ /**
310
+ * Fetches the record's data from the remote DWN using an anonymous `RecordsRead`.
311
+ */
312
+ private async readRecordData(): Promise<ReadableStream> {
313
+ try {
314
+ const reply = await this._anonymousDwn.recordsRead(this._remoteOrigin, {
315
+ filter: { recordId: this._recordId },
316
+ });
317
+
318
+ if (reply.status.code !== 200 || !reply.entry?.data) {
319
+ throw new Error(`${reply.status.code}: ${reply.status.detail}`);
320
+ }
321
+
322
+ return reply.entry.data;
323
+ } catch (error: unknown) {
324
+ const message = (error instanceof Error) ? error.message : 'Unknown error';
325
+ throw new Error(`ReadOnlyRecord: Error reading data for record '${this._recordId}': ${message}`);
326
+ }
327
+ }
328
+ }