@enbox/api 0.2.0 → 0.2.2
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 +470 -84
- package/dist/browser.mjs +11 -11
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/dwn-api.js +1 -7
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/types/dwn-api.d.ts +2 -4
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/dwn-api.ts +4 -11
package/README.md
CHANGED
|
@@ -4,7 +4,32 @@
|
|
|
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 protocol-first data management.
|
|
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
|
|
|
@@ -17,7 +42,7 @@ bun add @enbox/api
|
|
|
17
42
|
```ts
|
|
18
43
|
import { defineProtocol, Web5 } from '@enbox/api';
|
|
19
44
|
|
|
20
|
-
// 1. Connect
|
|
45
|
+
// 1. Connect -- creates or loads a local identity and agent
|
|
21
46
|
const { web5, did: myDid } = await Web5.connect();
|
|
22
47
|
|
|
23
48
|
// 2. Define a protocol with typed data shapes
|
|
@@ -40,15 +65,21 @@ const NotesProtocol = defineProtocol({
|
|
|
40
65
|
// 3. Scope all operations to the protocol
|
|
41
66
|
const notes = web5.using(NotesProtocol);
|
|
42
67
|
|
|
43
|
-
// 4. Install the protocol
|
|
68
|
+
// 4. Install the protocol on the local DWN
|
|
44
69
|
await notes.configure();
|
|
45
70
|
|
|
46
|
-
// 5. Write a record
|
|
71
|
+
// 5. Write a record -- path, data, and schema are type-checked
|
|
47
72
|
const { record } = await notes.records.write('note', {
|
|
48
73
|
data: { title: 'Hello', body: 'World' },
|
|
49
74
|
});
|
|
50
75
|
|
|
51
|
-
// 6.
|
|
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
|
|
52
83
|
await record.send(myDid);
|
|
53
84
|
```
|
|
54
85
|
|
|
@@ -56,46 +87,69 @@ await record.send(myDid);
|
|
|
56
87
|
|
|
57
88
|
### `Web5.connect(options?)`
|
|
58
89
|
|
|
59
|
-
Connects to a local identity agent
|
|
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.
|
|
60
91
|
|
|
61
92
|
```ts
|
|
62
|
-
const { web5, did, recoveryPhrase } = await Web5.connect(
|
|
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!
|
|
63
98
|
```
|
|
64
99
|
|
|
65
100
|
**Options** (all optional):
|
|
66
101
|
|
|
67
102
|
| Option | Type | Description |
|
|
68
103
|
|--------|------|-------------|
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `walletConnectOptions` | `ConnectOptions` | Trigger external wallet connect flow. |
|
|
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. |
|
|
76
112
|
|
|
77
113
|
**Returns** `{ web5, did, recoveryPhrase?, delegateDid? }`.
|
|
78
114
|
|
|
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
|
|
119
|
+
|
|
79
120
|
---
|
|
80
121
|
|
|
81
|
-
### `
|
|
122
|
+
### `defineProtocol(definition, schemaMap?)`
|
|
82
123
|
|
|
83
|
-
Creates a
|
|
124
|
+
Creates a typed protocol definition that enables compile-time path autocompletion and data type checking when used with `web5.using()`.
|
|
84
125
|
|
|
85
126
|
```ts
|
|
86
|
-
|
|
127
|
+
import type { ProtocolDefinition } from '@enbox/dwn-sdk-js';
|
|
87
128
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
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 };
|
|
91
144
|
});
|
|
92
|
-
|
|
93
|
-
for (const record of records) {
|
|
94
|
-
console.log(record.id, await record.data.text());
|
|
95
|
-
}
|
|
96
145
|
```
|
|
97
146
|
|
|
98
|
-
|
|
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.
|
|
148
|
+
|
|
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
|
|
99
153
|
|
|
100
154
|
---
|
|
101
155
|
|
|
@@ -104,51 +158,99 @@ Returns a `{ dwn: DwnReaderApi }` with read-only `records.query()` and `records.
|
|
|
104
158
|
The **primary interface** for all record operations. Returns a `TypedWeb5` instance scoped to the given protocol.
|
|
105
159
|
|
|
106
160
|
```ts
|
|
107
|
-
const
|
|
161
|
+
const chat = web5.using(ChatProtocol);
|
|
108
162
|
```
|
|
109
163
|
|
|
110
|
-
|
|
164
|
+
#### `configure()`
|
|
111
165
|
|
|
112
|
-
|
|
113
|
-
- **`notes.records.write(path, request)`** -- Write a record at a protocol path.
|
|
114
|
-
- **`notes.records.query(path, request?)`** -- Query records at a path.
|
|
115
|
-
- **`notes.records.read(path, request)`** -- Read a single record.
|
|
116
|
-
- **`notes.records.delete(path, request)`** -- Delete a record by ID.
|
|
117
|
-
- **`notes.records.subscribe(path, request?)`** -- Subscribe to real-time changes (returns a `LiveQuery`).
|
|
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.
|
|
118
167
|
|
|
119
|
-
|
|
168
|
+
```ts
|
|
169
|
+
await chat.configure();
|
|
170
|
+
```
|
|
120
171
|
|
|
121
|
-
|
|
172
|
+
#### `records.write(path, request)`
|
|
122
173
|
|
|
123
|
-
|
|
174
|
+
Write a record at a protocol path. The protocol URI, protocolPath, schema, and dataFormat are automatically injected.
|
|
124
175
|
|
|
125
|
-
|
|
176
|
+
```ts
|
|
177
|
+
const { record, status } = await chat.records.write('thread', {
|
|
178
|
+
data: { title: 'General', description: 'General discussion' },
|
|
179
|
+
});
|
|
180
|
+
|
|
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.
|
|
126
188
|
|
|
127
189
|
```ts
|
|
128
|
-
|
|
190
|
+
const { records, cursor } = await chat.records.query('thread', {
|
|
191
|
+
dateSort : 'createdDescending',
|
|
192
|
+
pagination : { limit: 20 },
|
|
193
|
+
});
|
|
129
194
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### `records.read(path, request)`
|
|
208
|
+
|
|
209
|
+
Read a single record by filter criteria.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const { record } = await chat.records.read('thread', {
|
|
213
|
+
filter: { recordId: 'bafyrei...' },
|
|
148
214
|
});
|
|
215
|
+
|
|
216
|
+
const data = await record.data.json();
|
|
149
217
|
```
|
|
150
218
|
|
|
151
|
-
|
|
219
|
+
#### `records.delete(path, request)`
|
|
220
|
+
|
|
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...',
|
|
252
|
+
});
|
|
253
|
+
```
|
|
152
254
|
|
|
153
255
|
---
|
|
154
256
|
|
|
@@ -156,74 +258,358 @@ The `schemaMap` is a phantom type -- it exists only at compile time. Pass `{} as
|
|
|
156
258
|
|
|
157
259
|
Methods like `write`, `query`, and `read` return `Record` instances.
|
|
158
260
|
|
|
159
|
-
**Properties**:
|
|
160
|
-
|
|
161
|
-
|
|
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:
|
|
162
284
|
|
|
163
285
|
```ts
|
|
164
|
-
await record.data.text(); // string
|
|
165
|
-
await record.data.json<MyType>(); //
|
|
166
|
-
await record.data.blob(); // Blob
|
|
167
|
-
await record.data.bytes(); // Uint8Array
|
|
168
|
-
await record.data.stream(); // ReadableStream
|
|
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
|
|
169
291
|
```
|
|
170
292
|
|
|
171
|
-
**Mutators
|
|
293
|
+
**Mutators**:
|
|
172
294
|
|
|
173
295
|
```ts
|
|
174
|
-
|
|
296
|
+
// Update the record's data
|
|
297
|
+
const { record: updated } = await record.update({
|
|
298
|
+
data: { title: 'Updated Title', body: '...' },
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Delete the record
|
|
175
302
|
const { status } = await record.delete();
|
|
176
303
|
```
|
|
177
304
|
|
|
178
|
-
**Side-effect methods
|
|
305
|
+
**Side-effect methods**:
|
|
179
306
|
|
|
180
307
|
```ts
|
|
181
|
-
|
|
182
|
-
await record.
|
|
183
|
-
|
|
308
|
+
// Send the record to a remote DWN
|
|
309
|
+
await record.send(targetDid);
|
|
310
|
+
|
|
311
|
+
// Persist a remote record to the local DWN
|
|
312
|
+
await record.store();
|
|
313
|
+
|
|
314
|
+
// Import a record from a remote DWN into the local store
|
|
315
|
+
await record.import();
|
|
184
316
|
```
|
|
185
317
|
|
|
186
318
|
---
|
|
187
319
|
|
|
188
320
|
### LiveQuery (Subscriptions)
|
|
189
321
|
|
|
190
|
-
`records.subscribe()` returns a `LiveQuery` that provides an initial snapshot plus real-time deduplicated change events.
|
|
322
|
+
`records.subscribe()` returns a `LiveQuery` that provides an initial snapshot of existing records plus a real-time stream of deduplicated change events.
|
|
191
323
|
|
|
192
324
|
```ts
|
|
193
|
-
const { liveQuery } = await
|
|
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}`);
|
|
340
|
+
});
|
|
194
341
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
liveQuery.on('delete', (record) => { /* deleted record */ });
|
|
342
|
+
// Unsubscribe from a specific handler
|
|
343
|
+
offCreate();
|
|
198
344
|
|
|
199
|
-
//
|
|
345
|
+
// Close the subscription entirely
|
|
200
346
|
await liveQuery.close();
|
|
201
347
|
```
|
|
202
348
|
|
|
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.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
### `Web5.anonymous(options?)`
|
|
356
|
+
|
|
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
|
+
});
|
|
377
|
+
|
|
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...',
|
|
387
|
+
});
|
|
388
|
+
```
|
|
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
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
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: {},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
} as const, {} as {
|
|
414
|
+
thread : { title: string };
|
|
415
|
+
message : { text: string };
|
|
416
|
+
});
|
|
417
|
+
|
|
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
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
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.
|
|
469
|
+
|
|
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
|
+
```
|
|
494
|
+
|
|
495
|
+
### Reading Public Data Anonymously
|
|
496
|
+
|
|
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',
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
for (const record of records) {
|
|
509
|
+
const note = await record.data.json();
|
|
510
|
+
console.log(note.title);
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Sending Records to Remote DWNs
|
|
515
|
+
|
|
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.
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
// Explicitly send to your own remote DWN
|
|
520
|
+
await record.send(myDid);
|
|
521
|
+
|
|
522
|
+
// Send to someone else's DWN (requires protocol permissions)
|
|
523
|
+
await record.send('did:dht:bob...');
|
|
524
|
+
```
|
|
525
|
+
|
|
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.
|
|
527
|
+
|
|
203
528
|
---
|
|
204
529
|
|
|
205
530
|
## Advanced Usage
|
|
206
531
|
|
|
207
|
-
|
|
532
|
+
### Unscoped DWN Access
|
|
533
|
+
|
|
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:
|
|
208
535
|
|
|
209
536
|
```ts
|
|
210
537
|
import { DwnApi } from '@enbox/api/advanced';
|
|
211
538
|
```
|
|
212
539
|
|
|
213
|
-
The `DwnApi` class provides raw `records`, `protocols`, and `permissions` accessors without protocol
|
|
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.
|
|
214
541
|
|
|
215
|
-
|
|
542
|
+
### Permissions
|
|
216
543
|
|
|
217
|
-
|
|
544
|
+
The DWN permission system supports fine-grained access control through permission requests, grants, and revocations.
|
|
218
545
|
|
|
219
546
|
```ts
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
566
|
|
|
223
|
-
|
|
567
|
+
```ts
|
|
568
|
+
// Resolve any DID
|
|
224
569
|
const { didDocument } = await web5.did.resolve('did:dht:abc...');
|
|
225
570
|
```
|
|
226
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
|
+
|
|
227
613
|
## License
|
|
228
614
|
|
|
229
615
|
Apache-2.0
|