@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.
- package/README.md +505 -138
- package/dist/browser.mjs +23 -13
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/advanced.js +11 -0
- package/dist/esm/advanced.js.map +1 -0
- package/dist/esm/define-protocol.js +3 -3
- package/dist/esm/dwn-api.js +56 -114
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-reader-api.js +128 -0
- package/dist/esm/dwn-reader-api.js.map +1 -0
- package/dist/esm/index.js +3 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-query.js +5 -4
- package/dist/esm/live-query.js.map +1 -1
- package/dist/esm/read-only-record.js +255 -0
- package/dist/esm/read-only-record.js.map +1 -0
- package/dist/esm/record.js +46 -57
- package/dist/esm/record.js.map +1 -1
- package/dist/esm/typed-web5.js +242 -0
- package/dist/esm/typed-web5.js.map +1 -0
- package/dist/esm/web5.js +71 -3
- package/dist/esm/web5.js.map +1 -1
- package/dist/types/advanced.d.ts +12 -0
- package/dist/types/advanced.d.ts.map +1 -0
- package/dist/types/define-protocol.d.ts +3 -3
- package/dist/types/dwn-api.d.ts +13 -92
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-reader-api.d.ts +138 -0
- package/dist/types/dwn-reader-api.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/live-query.d.ts +13 -1
- package/dist/types/live-query.d.ts.map +1 -1
- package/dist/types/protocol-types.d.ts +2 -2
- package/dist/types/read-only-record.d.ts +133 -0
- package/dist/types/read-only-record.d.ts.map +1 -0
- package/dist/types/record.d.ts +37 -17
- package/dist/types/record.d.ts.map +1 -1
- package/dist/types/{typed-dwn-api.d.ts → typed-web5.d.ts} +70 -73
- package/dist/types/typed-web5.d.ts.map +1 -0
- package/dist/types/web5.d.ts +79 -3
- package/dist/types/web5.d.ts.map +1 -1
- package/package.json +10 -6
- package/src/advanced.ts +29 -0
- package/src/define-protocol.ts +3 -3
- package/src/dwn-api.ts +91 -232
- package/src/dwn-reader-api.ts +255 -0
- package/src/index.ts +3 -2
- package/src/live-query.ts +20 -4
- package/src/protocol-types.ts +2 -2
- package/src/read-only-record.ts +328 -0
- package/src/record.ts +116 -86
- package/src/typed-web5.ts +445 -0
- package/src/web5.ts +104 -5
- package/dist/esm/typed-dwn-api.js +0 -182
- package/dist/esm/typed-dwn-api.js.map +0 -1
- package/dist/types/typed-dwn-api.d.ts.map +0 -1
- package/src/typed-dwn-api.ts +0 -370
package/README.md
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @enbox/api
|
|
2
2
|
|
|
3
|
-
> **Research Preview**
|
|
3
|
+
> **Research Preview** -- Enbox is under active development. APIs may change without notice.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/enboxorg/enbox/actions/workflows/ci.yml)
|
|
6
6
|
|
|
7
|
-
The high-level SDK for building decentralized applications with
|
|
7
|
+
The high-level SDK for building decentralized applications with protocol-first data management. This is the main consumer-facing package in the Enbox ecosystem -- most applications only need `@enbox/api`.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Quick Start](#quick-start)
|
|
13
|
+
- [Core Concepts](#core-concepts)
|
|
14
|
+
- [Web5.connect()](#web5connectoptions)
|
|
15
|
+
- [defineProtocol()](#defineprotocoldefinition-schemamap)
|
|
16
|
+
- [web5.using()](#web5usingprotocol)
|
|
17
|
+
- [Record Instances](#record-instances)
|
|
18
|
+
- [LiveQuery (Subscriptions)](#livequery-subscriptions)
|
|
19
|
+
- [Web5.anonymous()](#web5anonymousoptions)
|
|
20
|
+
- [Cookbook](#cookbook)
|
|
21
|
+
- [Nested Records](#nested-records)
|
|
22
|
+
- [Querying with Filters and Pagination](#querying-with-filters-and-pagination)
|
|
23
|
+
- [Tags](#tags)
|
|
24
|
+
- [Publishing Records](#publishing-records)
|
|
25
|
+
- [Reading Public Data Anonymously](#reading-public-data-anonymously)
|
|
26
|
+
- [Sending Records to Remote DWNs](#sending-records-to-remote-dwns)
|
|
27
|
+
- [Advanced Usage](#advanced-usage)
|
|
28
|
+
- [Unscoped DWN Access](#unscoped-dwn-access)
|
|
29
|
+
- [Permissions](#permissions)
|
|
30
|
+
- [DID Operations](#did-operations)
|
|
31
|
+
- [API Reference](#api-reference)
|
|
32
|
+
- [License](#license)
|
|
8
33
|
|
|
9
34
|
## Installation
|
|
10
35
|
|
|
@@ -14,235 +39,577 @@ bun add @enbox/api
|
|
|
14
39
|
|
|
15
40
|
## Quick Start
|
|
16
41
|
|
|
17
|
-
```
|
|
18
|
-
import {
|
|
42
|
+
```ts
|
|
43
|
+
import { defineProtocol, Web5 } from '@enbox/api';
|
|
19
44
|
|
|
20
|
-
|
|
45
|
+
// 1. Connect -- creates or loads a local identity and agent
|
|
46
|
+
const { web5, did: myDid } = await Web5.connect();
|
|
21
47
|
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
// 2. Define a protocol with typed data shapes
|
|
49
|
+
const NotesProtocol = defineProtocol({
|
|
50
|
+
protocol : 'https://example.com/notes',
|
|
51
|
+
published : true,
|
|
52
|
+
types : {
|
|
53
|
+
note: {
|
|
54
|
+
schema : 'https://example.com/schemas/note',
|
|
55
|
+
dataFormats : ['application/json'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
structure: {
|
|
59
|
+
note: {},
|
|
60
|
+
},
|
|
61
|
+
} as const, {} as {
|
|
62
|
+
note: { title: string; body: string };
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 3. Scope all operations to the protocol
|
|
66
|
+
const notes = web5.using(NotesProtocol);
|
|
67
|
+
|
|
68
|
+
// 4. Install the protocol on the local DWN
|
|
69
|
+
await notes.configure();
|
|
70
|
+
|
|
71
|
+
// 5. Write a record -- path, data, and schema are type-checked
|
|
72
|
+
const { record } = await notes.records.write('note', {
|
|
73
|
+
data: { title: 'Hello', body: 'World' },
|
|
26
74
|
});
|
|
27
75
|
|
|
28
|
-
//
|
|
76
|
+
// 6. Query records back
|
|
77
|
+
const { records } = await notes.records.query('note');
|
|
78
|
+
for (const r of records) {
|
|
79
|
+
console.log(r.id, await r.data.json());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 7. Send to your remote DWN
|
|
29
83
|
await record.send(myDid);
|
|
30
84
|
```
|
|
31
85
|
|
|
32
|
-
##
|
|
86
|
+
## Core Concepts
|
|
33
87
|
|
|
34
|
-
###
|
|
88
|
+
### `Web5.connect(options?)`
|
|
35
89
|
|
|
36
|
-
Connects to a
|
|
90
|
+
Connects to a local identity agent. On first launch it creates an identity vault, generates a `did:dht` DID, and starts the sync engine. On subsequent launches it unlocks the existing vault.
|
|
37
91
|
|
|
38
|
-
```
|
|
39
|
-
const {
|
|
92
|
+
```ts
|
|
93
|
+
const { web5, did, recoveryPhrase } = await Web5.connect({
|
|
94
|
+
password: 'user-chosen-password',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// recoveryPhrase is returned on first launch only -- store it safely!
|
|
40
98
|
```
|
|
41
99
|
|
|
42
|
-
|
|
100
|
+
**Options** (all optional):
|
|
43
101
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
102
|
+
| Option | Type | Description |
|
|
103
|
+
|--------|------|-------------|
|
|
104
|
+
| `password` | `string` | Password to protect the local identity vault. **Defaults to an insecure static value** -- always set this in production. |
|
|
105
|
+
| `recoveryPhrase` | `string` | 12-word BIP-39 phrase for vault recovery. Generated automatically on first launch if not provided. |
|
|
106
|
+
| `sync` | `string` | Sync interval (e.g. `'2m'`, `'30s'`) or `'off'` to disable. Default: `'2m'`. |
|
|
107
|
+
| `didCreateOptions.dwnEndpoints` | `string[]` | DWN service endpoints for the created DID. Default: `['https://enbox-dwn.fly.dev']`. |
|
|
108
|
+
| `connectedDid` | `string` | Use an existing DID instead of creating a new one. |
|
|
109
|
+
| `agent` | `Web5Agent` | Provide a custom agent instance. Defaults to a local `Web5UserAgent`. |
|
|
110
|
+
| `walletConnectOptions` | `ConnectOptions` | Trigger an external wallet connect flow for delegated identity. |
|
|
111
|
+
| `registration` | `{ onSuccess, onFailure }` | Callbacks for DWN endpoint registration status. |
|
|
48
112
|
|
|
49
|
-
|
|
113
|
+
**Returns** `{ web5, did, recoveryPhrase?, delegateDid? }`.
|
|
50
114
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
115
|
+
- `web5` -- the `Web5` instance for all subsequent operations
|
|
116
|
+
- `did` -- the connected DID URI (e.g. `did:dht:abc...`)
|
|
117
|
+
- `recoveryPhrase` -- only returned on first initialization
|
|
118
|
+
- `delegateDid` -- only present when using wallet connect
|
|
53
119
|
|
|
54
120
|
---
|
|
55
121
|
|
|
56
|
-
###
|
|
122
|
+
### `defineProtocol(definition, schemaMap?)`
|
|
123
|
+
|
|
124
|
+
Creates a typed protocol definition that enables compile-time path autocompletion and data type checking when used with `web5.using()`.
|
|
57
125
|
|
|
58
|
-
|
|
126
|
+
```ts
|
|
127
|
+
import type { ProtocolDefinition } from '@enbox/dwn-sdk-js';
|
|
128
|
+
|
|
129
|
+
const ChatProtocol = defineProtocol({
|
|
130
|
+
protocol : 'https://example.com/chat',
|
|
131
|
+
published : true,
|
|
132
|
+
types: {
|
|
133
|
+
thread : { schema: 'https://example.com/schemas/thread', dataFormats: ['application/json'] },
|
|
134
|
+
message : { schema: 'https://example.com/schemas/message', dataFormats: ['application/json'] },
|
|
135
|
+
},
|
|
136
|
+
structure: {
|
|
137
|
+
thread: {
|
|
138
|
+
message: {}, // messages are nested under threads
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
} as const satisfies ProtocolDefinition, {} as {
|
|
142
|
+
thread : { title: string; description?: string };
|
|
143
|
+
message : { text: string };
|
|
144
|
+
});
|
|
145
|
+
```
|
|
59
146
|
|
|
60
|
-
**
|
|
147
|
+
The second argument is a **phantom type** -- it only exists at compile time. Pass `{} as YourSchemaMap` to map protocol type names to TypeScript interfaces. The runtime value is ignored.
|
|
61
148
|
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
149
|
+
This gives you:
|
|
150
|
+
- **Path autocompletion**: `'thread'`, `'thread/message'` are inferred from `structure`
|
|
151
|
+
- **Typed `data` payloads**: `write('thread', { data: ... })` type-checks against the schema map
|
|
152
|
+
- **Typed `dataFormat`**: restricted to the formats declared in the protocol type
|
|
66
153
|
|
|
67
154
|
---
|
|
68
155
|
|
|
69
|
-
###
|
|
156
|
+
### `web5.using(protocol)`
|
|
70
157
|
|
|
71
|
-
|
|
158
|
+
The **primary interface** for all record operations. Returns a `TypedWeb5` instance scoped to the given protocol.
|
|
72
159
|
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
160
|
+
```ts
|
|
161
|
+
const chat = web5.using(ChatProtocol);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `configure()`
|
|
165
|
+
|
|
166
|
+
Installs the protocol on the local DWN. If already installed with an identical definition, this is a no-op. If the definition has changed, it reconfigures with the updated version.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
await chat.configure();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### `records.write(path, request)`
|
|
173
|
+
|
|
174
|
+
Write a record at a protocol path. The protocol URI, protocolPath, schema, and dataFormat are automatically injected.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const { record, status } = await chat.records.write('thread', {
|
|
178
|
+
data: { title: 'General', description: 'General discussion' },
|
|
82
179
|
});
|
|
83
180
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
181
|
+
console.log(status.code); // 202
|
|
182
|
+
console.log(record.id); // unique record ID
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### `records.query(path, request?)`
|
|
186
|
+
|
|
187
|
+
Query records at a protocol path. Returns matching records with optional pagination.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const { records, cursor } = await chat.records.query('thread', {
|
|
191
|
+
dateSort : 'createdDescending',
|
|
192
|
+
pagination : { limit: 20 },
|
|
94
193
|
});
|
|
194
|
+
|
|
195
|
+
for (const thread of records) {
|
|
196
|
+
console.log(await thread.data.json());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Fetch next page
|
|
200
|
+
if (cursor) {
|
|
201
|
+
const { records: nextPage } = await chat.records.query('thread', {
|
|
202
|
+
pagination: { limit: 20, cursor },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
95
205
|
```
|
|
96
206
|
|
|
97
|
-
|
|
207
|
+
#### `records.read(path, request)`
|
|
98
208
|
|
|
99
|
-
|
|
209
|
+
Read a single record by filter criteria.
|
|
100
210
|
|
|
101
|
-
|
|
211
|
+
```ts
|
|
212
|
+
const { record } = await chat.records.read('thread', {
|
|
213
|
+
filter: { recordId: 'bafyrei...' },
|
|
214
|
+
});
|
|
102
215
|
|
|
103
|
-
|
|
216
|
+
const data = await record.data.json();
|
|
217
|
+
```
|
|
104
218
|
|
|
105
|
-
|
|
219
|
+
#### `records.delete(path, request)`
|
|
106
220
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
221
|
+
Delete a record by ID.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const { status } = await chat.records.delete('thread', {
|
|
225
|
+
recordId: record.id,
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### `records.subscribe(path, request?)`
|
|
230
|
+
|
|
231
|
+
Subscribe to real-time changes. Returns a `LiveQuery` with an initial snapshot plus a stream of change events.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const { liveQuery } = await chat.records.subscribe('thread/message');
|
|
235
|
+
|
|
236
|
+
// Initial snapshot
|
|
237
|
+
for (const msg of liveQuery.records) {
|
|
238
|
+
console.log(await msg.data.json());
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Real-time updates
|
|
242
|
+
liveQuery.on('create', (record) => console.log('new:', record.id));
|
|
243
|
+
liveQuery.on('update', (record) => console.log('updated:', record.id));
|
|
244
|
+
liveQuery.on('delete', (record) => console.log('deleted:', record.id));
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
All methods also accept a `from` option to query a remote DWN:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const { records } = await chat.records.query('thread', {
|
|
251
|
+
from: 'did:dht:other-user...',
|
|
115
252
|
});
|
|
116
253
|
```
|
|
117
254
|
|
|
118
255
|
---
|
|
119
256
|
|
|
120
|
-
###
|
|
257
|
+
### Record Instances
|
|
258
|
+
|
|
259
|
+
Methods like `write`, `query`, and `read` return `Record` instances.
|
|
260
|
+
|
|
261
|
+
**Properties**:
|
|
262
|
+
|
|
263
|
+
| Property | Description |
|
|
264
|
+
|----------|-------------|
|
|
265
|
+
| `id` | Unique record identifier |
|
|
266
|
+
| `contextId` | Context ID (scopes nested records to a parent thread) |
|
|
267
|
+
| `protocol` | Protocol URI |
|
|
268
|
+
| `protocolPath` | Path within the protocol structure (e.g. `'thread/message'`) |
|
|
269
|
+
| `schema` | Schema URI |
|
|
270
|
+
| `dataFormat` | MIME type of the data |
|
|
271
|
+
| `dataCid` | Content-addressed hash of the data |
|
|
272
|
+
| `dataSize` | Size of the data in bytes |
|
|
273
|
+
| `dateCreated` | ISO timestamp of creation |
|
|
274
|
+
| `timestamp` | ISO timestamp of most recent write |
|
|
275
|
+
| `datePublished` | ISO timestamp of publication (if published) |
|
|
276
|
+
| `published` | Whether the record is publicly readable |
|
|
277
|
+
| `author` | DID of the record author |
|
|
278
|
+
| `recipient` | DID of the intended recipient |
|
|
279
|
+
| `parentId` | Record ID of the parent record (for nested structures) |
|
|
280
|
+
| `tags` | Key-value metadata tags |
|
|
281
|
+
| `deleted` | Whether the record has been deleted |
|
|
282
|
+
|
|
283
|
+
**Data accessors** -- read the record payload in different formats:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const text = await record.data.text(); // string
|
|
287
|
+
const obj = await record.data.json<MyType>(); // parsed JSON (typed)
|
|
288
|
+
const blob = await record.data.blob(); // Blob
|
|
289
|
+
const bytes = await record.data.bytes(); // Uint8Array
|
|
290
|
+
const stream = await record.data.stream(); // ReadableStream
|
|
291
|
+
```
|
|
121
292
|
|
|
122
|
-
|
|
293
|
+
**Mutators**:
|
|
123
294
|
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
295
|
+
```ts
|
|
296
|
+
// Update the record's data
|
|
297
|
+
const { record: updated } = await record.update({
|
|
298
|
+
data: { title: 'Updated Title', body: '...' },
|
|
128
299
|
});
|
|
129
300
|
|
|
130
|
-
|
|
131
|
-
await record.
|
|
301
|
+
// Delete the record
|
|
302
|
+
const { status } = await record.delete();
|
|
132
303
|
```
|
|
133
304
|
|
|
134
|
-
|
|
305
|
+
**Side-effect methods**:
|
|
135
306
|
|
|
136
|
-
|
|
307
|
+
```ts
|
|
308
|
+
// Send the record to a remote DWN
|
|
309
|
+
await record.send(targetDid);
|
|
137
310
|
|
|
138
|
-
|
|
311
|
+
// Persist a remote record to the local DWN
|
|
312
|
+
await record.store();
|
|
139
313
|
|
|
140
|
-
|
|
314
|
+
// Import a record from a remote DWN into the local store
|
|
315
|
+
await record.import();
|
|
316
|
+
```
|
|
141
317
|
|
|
142
318
|
---
|
|
143
319
|
|
|
144
|
-
###
|
|
320
|
+
### LiveQuery (Subscriptions)
|
|
145
321
|
|
|
146
|
-
|
|
322
|
+
`records.subscribe()` returns a `LiveQuery` that provides an initial snapshot of existing records plus a real-time stream of deduplicated change events.
|
|
147
323
|
|
|
148
|
-
```
|
|
149
|
-
const {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
324
|
+
```ts
|
|
325
|
+
const { liveQuery } = await chat.records.subscribe('thread/message');
|
|
326
|
+
|
|
327
|
+
// Initial snapshot
|
|
328
|
+
for (const msg of liveQuery.records) {
|
|
329
|
+
renderMessage(msg);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Real-time changes
|
|
333
|
+
const offCreate = liveQuery.on('create', (record) => appendMessage(record));
|
|
334
|
+
const offUpdate = liveQuery.on('update', (record) => refreshMessage(record));
|
|
335
|
+
const offDelete = liveQuery.on('delete', (record) => removeMessage(record));
|
|
336
|
+
|
|
337
|
+
// Catch-all event (receives { type, record })
|
|
338
|
+
liveQuery.on('change', ({ type, record }) => {
|
|
339
|
+
console.log(`${type}: ${record.id}`);
|
|
153
340
|
});
|
|
154
341
|
|
|
155
|
-
|
|
342
|
+
// Unsubscribe from a specific handler
|
|
343
|
+
offCreate();
|
|
344
|
+
|
|
345
|
+
// Close the subscription entirely
|
|
346
|
+
await liveQuery.close();
|
|
156
347
|
```
|
|
157
348
|
|
|
158
|
-
|
|
349
|
+
`LiveQuery` extends `EventTarget`, so standard `addEventListener` / `removeEventListener` also work. The `.on()` method is a convenience wrapper that returns an unsubscribe function.
|
|
350
|
+
|
|
351
|
+
Events are automatically deduplicated against the initial snapshot -- you won't receive a `create` event for records already in the `records` array.
|
|
159
352
|
|
|
160
353
|
---
|
|
161
354
|
|
|
162
|
-
###
|
|
355
|
+
### `Web5.anonymous(options?)`
|
|
163
356
|
|
|
164
|
-
|
|
357
|
+
Creates a lightweight, read-only instance for querying public DWN data. No identity, vault, or signing keys are required.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
const { dwn } = Web5.anonymous();
|
|
361
|
+
|
|
362
|
+
// Query published records from someone's DWN
|
|
363
|
+
const { records } = await dwn.records.query({
|
|
364
|
+
from : 'did:dht:alice...',
|
|
365
|
+
filter : { protocol: 'https://example.com/notes', protocolPath: 'note' },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
for (const record of records) {
|
|
369
|
+
console.log(record.id, await record.data.text());
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Read a specific record
|
|
373
|
+
const { record } = await dwn.records.read({
|
|
374
|
+
from : 'did:dht:alice...',
|
|
375
|
+
filter : { recordId: 'bafyrei...' },
|
|
376
|
+
});
|
|
165
377
|
|
|
166
|
-
|
|
167
|
-
await
|
|
168
|
-
|
|
378
|
+
// Count matching records
|
|
379
|
+
const { count } = await dwn.records.count({
|
|
380
|
+
from : 'did:dht:alice...',
|
|
381
|
+
filter : { protocol: 'https://example.com/notes', protocolPath: 'note' },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Query published protocols
|
|
385
|
+
const { protocols } = await dwn.protocols.query({
|
|
386
|
+
from: 'did:dht:alice...',
|
|
169
387
|
});
|
|
170
388
|
```
|
|
171
389
|
|
|
390
|
+
Returns `ReadOnlyRecord` instances (no `update`, `delete`, `send`, or `store` methods). All calls require a `from` DID since the reader has no local DWN.
|
|
391
|
+
|
|
172
392
|
---
|
|
173
393
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
$actions: [{ who: 'recipient', can: 'read' }],
|
|
192
|
-
},
|
|
193
|
-
photo: {
|
|
194
|
-
$actions: [{ who: 'recipient', can: 'read' }],
|
|
195
|
-
image: {
|
|
196
|
-
$actions: [{ who: 'author', of: 'photo', can: 'write' }],
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
},
|
|
394
|
+
## Cookbook
|
|
395
|
+
|
|
396
|
+
### Nested Records
|
|
397
|
+
|
|
398
|
+
Protocols support hierarchical record structures. Child records reference their parent via `parentContextId`.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
const ChatProtocol = defineProtocol({
|
|
402
|
+
protocol : 'https://example.com/chat',
|
|
403
|
+
published : true,
|
|
404
|
+
types: {
|
|
405
|
+
thread : { schema: 'https://example.com/schemas/thread', dataFormats: ['application/json'] },
|
|
406
|
+
message : { schema: 'https://example.com/schemas/message', dataFormats: ['application/json'] },
|
|
407
|
+
},
|
|
408
|
+
structure: {
|
|
409
|
+
thread: {
|
|
410
|
+
message: {},
|
|
200
411
|
},
|
|
201
412
|
},
|
|
413
|
+
} as const, {} as {
|
|
414
|
+
thread : { title: string };
|
|
415
|
+
message : { text: string };
|
|
202
416
|
});
|
|
203
417
|
|
|
204
|
-
|
|
418
|
+
const chat = web5.using(ChatProtocol);
|
|
419
|
+
await chat.configure();
|
|
420
|
+
|
|
421
|
+
// Create a parent thread
|
|
422
|
+
const { record: thread } = await chat.records.write('thread', {
|
|
423
|
+
data: { title: 'General' },
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Write a message nested under the thread
|
|
427
|
+
const { record: msg } = await chat.records.write('thread/message', {
|
|
428
|
+
parentContextId : thread.contextId,
|
|
429
|
+
data : { text: 'Hello, world!' },
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Query messages within a specific thread
|
|
433
|
+
const { records: messages } = await chat.records.query('thread/message', {
|
|
434
|
+
filter: { parentId: thread.id },
|
|
435
|
+
});
|
|
205
436
|
```
|
|
206
437
|
|
|
207
|
-
|
|
438
|
+
### Querying with Filters and Pagination
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
// Date-sorted, paginated query
|
|
442
|
+
const { records, cursor } = await notes.records.query('note', {
|
|
443
|
+
dateSort : 'createdDescending',
|
|
444
|
+
pagination : { limit: 10 },
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Fetch next page using the cursor
|
|
448
|
+
if (cursor) {
|
|
449
|
+
const { records: page2 } = await notes.records.query('note', {
|
|
450
|
+
dateSort : 'createdDescending',
|
|
451
|
+
pagination : { limit: 10, cursor },
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Filter by recipient
|
|
456
|
+
const { records: shared } = await notes.records.query('note', {
|
|
457
|
+
filter: { recipient: 'did:dht:bob...' },
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Query from a remote DWN
|
|
461
|
+
const { records: remote } = await notes.records.query('note', {
|
|
462
|
+
from: 'did:dht:alice...',
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Tags
|
|
467
|
+
|
|
468
|
+
Tags are key-value metadata attached to records, useful for filtering without parsing record data.
|
|
208
469
|
|
|
209
|
-
|
|
470
|
+
```ts
|
|
471
|
+
const { record } = await notes.records.write('note', {
|
|
472
|
+
data : { title: 'Meeting Notes', body: '...' },
|
|
473
|
+
tags : { category: 'work', priority: 'high' },
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Query by tag
|
|
477
|
+
const { records } = await notes.records.query('note', {
|
|
478
|
+
filter: { tags: { category: 'work' } },
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
> Note: tags must be declared in your protocol's type definition for the DWN engine to index them.
|
|
483
|
+
|
|
484
|
+
### Publishing Records
|
|
485
|
+
|
|
486
|
+
Published records are publicly readable by anyone, including anonymous readers.
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
const { record } = await notes.records.write('note', {
|
|
490
|
+
data : { title: 'Public Note', body: 'Visible to everyone' },
|
|
491
|
+
published : true,
|
|
492
|
+
});
|
|
493
|
+
```
|
|
210
494
|
|
|
211
|
-
|
|
495
|
+
### Reading Public Data Anonymously
|
|
212
496
|
|
|
213
|
-
```
|
|
214
|
-
const {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
497
|
+
```ts
|
|
498
|
+
const { dwn } = Web5.anonymous();
|
|
499
|
+
|
|
500
|
+
const { records } = await dwn.records.query({
|
|
501
|
+
from : 'did:dht:alice...',
|
|
502
|
+
filter : {
|
|
503
|
+
protocol : 'https://example.com/notes',
|
|
504
|
+
protocolPath : 'note',
|
|
218
505
|
},
|
|
219
506
|
});
|
|
507
|
+
|
|
508
|
+
for (const record of records) {
|
|
509
|
+
const note = await record.data.json();
|
|
510
|
+
console.log(note.title);
|
|
511
|
+
}
|
|
220
512
|
```
|
|
221
513
|
|
|
222
|
-
|
|
514
|
+
### Sending Records to Remote DWNs
|
|
223
515
|
|
|
224
|
-
|
|
516
|
+
Records are initially written to the local DWN. Use `send()` to push them to a remote DWN, or rely on the automatic sync engine.
|
|
225
517
|
|
|
226
|
-
|
|
518
|
+
```ts
|
|
519
|
+
// Explicitly send to your own remote DWN
|
|
520
|
+
await record.send(myDid);
|
|
227
521
|
|
|
228
|
-
|
|
229
|
-
|
|
522
|
+
// Send to someone else's DWN (requires protocol permissions)
|
|
523
|
+
await record.send('did:dht:bob...');
|
|
230
524
|
```
|
|
231
525
|
|
|
232
|
-
|
|
526
|
+
The sync engine (enabled by default at 2-minute intervals) automatically synchronizes records between local and remote DWNs. For most use cases, you don't need to call `send()` manually.
|
|
233
527
|
|
|
234
528
|
---
|
|
235
529
|
|
|
236
|
-
|
|
530
|
+
## Advanced Usage
|
|
237
531
|
|
|
238
|
-
|
|
532
|
+
### Unscoped DWN Access
|
|
239
533
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
534
|
+
For power users who need direct DWN access without protocol scoping (e.g. cross-protocol queries, raw permission management), import from the `@enbox/api/advanced` sub-path:
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
import { DwnApi } from '@enbox/api/advanced';
|
|
244
538
|
```
|
|
245
539
|
|
|
540
|
+
The `DwnApi` class provides raw `records`, `protocols`, and `permissions` accessors without automatic protocol/path/schema injection. You must provide those fields manually in every call. Most applications should use `web5.using()` instead.
|
|
541
|
+
|
|
542
|
+
### Permissions
|
|
543
|
+
|
|
544
|
+
The DWN permission system supports fine-grained access control through permission requests, grants, and revocations.
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
import { DwnApi } from '@enbox/api/advanced';
|
|
548
|
+
|
|
549
|
+
// Query existing permission grants
|
|
550
|
+
const grants = await web5._dwn.permissions.queryGrants();
|
|
551
|
+
|
|
552
|
+
// Request permissions from another DWN
|
|
553
|
+
const request = await web5._dwn.permissions.request({
|
|
554
|
+
scope: {
|
|
555
|
+
interface : 'Records',
|
|
556
|
+
method : 'Write',
|
|
557
|
+
protocol : 'https://example.com/notes',
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Send the request to the target DWN
|
|
562
|
+
await request.send('did:dht:alice...');
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### DID Operations
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
// Resolve any DID
|
|
569
|
+
const { didDocument } = await web5.did.resolve('did:dht:abc...');
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## API Reference
|
|
575
|
+
|
|
576
|
+
### Main Exports (`@enbox/api`)
|
|
577
|
+
|
|
578
|
+
| Export | Description |
|
|
579
|
+
|--------|-------------|
|
|
580
|
+
| `Web5` | Main entry point -- `connect()`, `anonymous()`, `using()` |
|
|
581
|
+
| `defineProtocol()` | Factory for creating typed protocol definitions |
|
|
582
|
+
| `TypedWeb5` | Protocol-scoped API returned by `web5.using()` |
|
|
583
|
+
| `Record` | Mutable record instance with data accessors and side-effect methods |
|
|
584
|
+
| `ReadOnlyRecord` | Immutable record for anonymous/read-only access |
|
|
585
|
+
| `LiveQuery` | Real-time subscription with initial snapshot and change events |
|
|
586
|
+
| `Protocol` | Protocol metadata wrapper |
|
|
587
|
+
| `PermissionGrant` | Permission grant record |
|
|
588
|
+
| `PermissionRequest` | Permission request record |
|
|
589
|
+
| `PermissionGrantRevocation` | Permission revocation record |
|
|
590
|
+
| `DidApi` | DID resolution |
|
|
591
|
+
| `VcApi` | Verifiable Credentials (not yet implemented) |
|
|
592
|
+
| `DwnReaderApi` | Read-only DWN API for anonymous access |
|
|
593
|
+
|
|
594
|
+
### Advanced Export (`@enbox/api/advanced`)
|
|
595
|
+
|
|
596
|
+
| Export | Description |
|
|
597
|
+
|--------|-------------|
|
|
598
|
+
| `DwnApi` | Full unscoped DWN API with `records`, `protocols`, `permissions` |
|
|
599
|
+
|
|
600
|
+
### Key Types
|
|
601
|
+
|
|
602
|
+
| Export | Description |
|
|
603
|
+
|--------|-------------|
|
|
604
|
+
| `TypedProtocol<D, M>` | Typed protocol wrapper with definition and schema map |
|
|
605
|
+
| `ProtocolPaths<D>` | Union of valid slash-delimited paths for a protocol definition |
|
|
606
|
+
| `SchemaMap` | Maps protocol type names to TypeScript interfaces |
|
|
607
|
+
| `Web5ConnectOptions` | Options for `Web5.connect()` |
|
|
608
|
+
| `Web5ConnectResult` | Return type of `Web5.connect()` |
|
|
609
|
+
| `RecordModel` | Structured data model of a record |
|
|
610
|
+
| `RecordChangeType` | `'create' \| 'update' \| 'delete'` |
|
|
611
|
+
| `RecordChange` | Change event payload `{ type, record }` |
|
|
612
|
+
|
|
246
613
|
## License
|
|
247
614
|
|
|
248
615
|
Apache-2.0
|