@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
package/README.md CHANGED
@@ -1,10 +1,35 @@
1
- # Enbox API SDK
1
+ # @enbox/api
2
2
 
3
- > **Research Preview** Enbox is under active development. APIs may change without notice.
3
+ > **Research Preview** -- Enbox is under active development. APIs may change without notice.
4
4
 
5
5
  [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/LiranCohen/02d15f39a46173a612a8862ec6d7cfcf/raw/api.json)](https://github.com/enboxorg/enbox/actions/workflows/ci.yml)
6
6
 
7
- The high-level SDK for building decentralized applications with identity and 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
 
@@ -14,235 +39,577 @@ bun add @enbox/api
14
39
 
15
40
  ## Quick Start
16
41
 
17
- ```javascript
18
- import { Enbox } from '@enbox/api';
42
+ ```ts
43
+ import { defineProtocol, Web5 } from '@enbox/api';
19
44
 
20
- const { enbox, did: myDid } = await Enbox.connect();
45
+ // 1. Connect -- creates or loads a local identity and agent
46
+ const { web5, did: myDid } = await Web5.connect();
21
47
 
22
- // Create a record
23
- const { record } = await enbox.dwn.records.create({
24
- data : 'Hello World!',
25
- message : { dataFormat: 'text/plain' },
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
- // Send it to your remote DWN
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
- ## API Documentation
86
+ ## Core Concepts
33
87
 
34
- ### **`Enbox.connect(options)`**
88
+ ### `Web5.connect(options?)`
35
89
 
36
- Connects to a user's local identity agent or generates an in-app DID.
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
- ```javascript
39
- const { enbox, did: myDid } = await Enbox.connect();
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
- #### Options (all optional)
100
+ **Options** (all optional):
43
101
 
44
- - **`agent`** - `EnboxAgent` instance. Defaults to a local `EnboxUserAgent`.
45
- - **`connectedDid`** - `string`: an existing DID to connect to.
46
- - **`sync`** - `string`: sync interval (any value accepted by [`ms`](https://www.npmjs.com/package/ms)), or `'off'` to disable. Default: `'2m'`.
47
- - **`techPreview.dwnEndpoints`** - `string[]`: DWN endpoints for the created DID. Default: `['https://enbox-dwn.fly.dev']`.
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
- #### Response
113
+ **Returns** `{ web5, did, recoveryPhrase?, delegateDid? }`.
50
114
 
51
- - **`enbox`** - `Enbox` instance with access to DWN operations and DID methods.
52
- - **`did`** - `string`: the DID that was created or connected to.
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
- ### Record Instances
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
- Methods like `create`, `write`, and `query` return `Record` instances with:
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
- **Properties**: `id`, `contextId`, `dataFormat`, `dateCreated`, `dateModified`, `datePublished`, `encryption`, `protocol`, `protocolPath`, `recipient`, `schema`, `dataCid`, `dataSize`, `published`.
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
- **Methods**:
63
- - **`data.blob()`** / **`data.bytes()`** / **`data.json()`** / **`data.stream()`** / **`data.text()`** - read record data in various formats.
64
- - **`send(did)`** - send the record to a DID's DWN endpoints.
65
- - **`update(request)`** - overwrite the record with new data.
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
- ### **`enbox.dwn.records.query(request)`**
156
+ ### `web5.using(protocol)`
70
157
 
71
- Query your own or another DID's DWN for records.
158
+ The **primary interface** for all record operations. Returns a `TypedWeb5` instance scoped to the given protocol.
72
159
 
73
- ```javascript
74
- // Query your own DWN
75
- const { records } = await enbox.dwn.records.query({
76
- message: {
77
- filter: {
78
- schema : 'https://schema.org/Playlist',
79
- dataFormat : 'application/json',
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
- // Query Bob's DWN
85
- const { records } = await enbox.dwn.records.query({
86
- from: 'did:example:bob',
87
- message: {
88
- filter: {
89
- protocol : 'https://music.org/protocol',
90
- schema : 'https://schema.org/Playlist',
91
- dataFormat : 'application/json',
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
- **Filter properties**: `recordId`, `protocol`, `protocolPath`, `contextId`, `parentId`, `recipient`, `schema`, `dataFormat`.
207
+ #### `records.read(path, request)`
98
208
 
99
- **Pagination**: `{ limit: number, cursor: string }`. The response includes a `cursor` if more results exist.
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
- ### **`enbox.dwn.records.subscribe(request)`**
216
+ const data = await record.data.json();
217
+ ```
104
218
 
105
- Subscribe to record changes on your own or another DID's DWN.
219
+ #### `records.delete(path, request)`
106
220
 
107
- ```javascript
108
- const { status } = await enbox.dwn.records.subscribe({
109
- message: {
110
- filter: { protocol: 'https://schema.org/protocols/social' },
111
- },
112
- subscriptionHandler: (record) => {
113
- console.log('received', record);
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
- ### **`enbox.dwn.records.create(request)`**
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
- Create a new record and optionally store it locally.
293
+ **Mutators**:
123
294
 
124
- ```javascript
125
- const { record } = await enbox.dwn.records.create({
126
- data : 'Hello World!',
127
- message : { dataFormat: 'text/plain' },
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
- await record.send(myDid); // send to your remote DWN
131
- await record.send('did:example:bob'); // send to Bob's DWN
301
+ // Delete the record
302
+ const { status } = await record.delete();
132
303
  ```
133
304
 
134
- Pass `store: false` to create without storing locally (e.g., for records you only send to others).
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
- ### **`enbox.dwn.records.write(request)`**
311
+ // Persist a remote record to the local DWN
312
+ await record.store();
139
313
 
140
- Alias for `create()` same request object.
314
+ // Import a record from a remote DWN into the local store
315
+ await record.import();
316
+ ```
141
317
 
142
318
  ---
143
319
 
144
- ### **`enbox.dwn.records.read(request)`**
320
+ ### LiveQuery (Subscriptions)
145
321
 
146
- Read a specific record by filter (most commonly `recordId`).
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
- ```javascript
149
- const { record } = await enbox.dwn.records.read({
150
- message: {
151
- filter: { recordId: 'bfw35evr6e54c4cqa4c589h4cq3v7w4nc534c9w7h5' },
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
- console.log(await record.data.text());
342
+ // Unsubscribe from a specific handler
343
+ offCreate();
344
+
345
+ // Close the subscription entirely
346
+ await liveQuery.close();
156
347
  ```
157
348
 
158
- Use `from: 'did:example:bob'` to read from another DID's DWN.
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
- ### **`enbox.dwn.records.delete(request)`**
355
+ ### `Web5.anonymous(options?)`
163
356
 
164
- Delete a record by ID.
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
- ```javascript
167
- await enbox.dwn.records.delete({
168
- message: { recordId: 'bfw35evr6e54c4cqa4c589h4cq3v7w4nc534c9w7h5' },
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
- ### **`enbox.dwn.protocols.configure(request)`**
175
-
176
- Install a protocol definition on your DWN.
177
-
178
- ```javascript
179
- const { protocol } = await enbox.dwn.protocols.configure({
180
- message: {
181
- definition: {
182
- protocol : 'https://photos.org/protocol',
183
- published : true,
184
- types: {
185
- album : { schema: 'https://photos.org/album', dataFormats: ['application/json'] },
186
- photo : { schema: 'https://photos.org/photo', dataFormats: ['application/json'] },
187
- image : { dataFormats: ['image/png', 'image/jpeg', 'image/gif'] },
188
- },
189
- structure: {
190
- album: {
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
- await protocol.send(myDid); // sync to remote DWNs
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
- ### **`enbox.dwn.protocols.query(request)`**
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
- Query a DID's DWN for installed protocols.
495
+ ### Reading Public Data Anonymously
212
496
 
213
- ```javascript
214
- const { protocols } = await enbox.dwn.protocols.query({
215
- from: 'did:example:bob',
216
- message: {
217
- filter: { protocol: 'https://music.org/protocol' },
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
- ### **`enbox.did.create(method, options)`**
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
- Generate a DID using a supported method (`'dht'` or `'jwk'`).
518
+ ```ts
519
+ // Explicitly send to your own remote DWN
520
+ await record.send(myDid);
227
521
 
228
- ```javascript
229
- const myDid = await enbox.did.create('dht');
522
+ // Send to someone else's DWN (requires protocol permissions)
523
+ await record.send('did:dht:bob...');
230
524
  ```
231
525
 
232
- Pass `store: false` in options to skip storing the DID's keys in the agent.
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
- ### **`enbox.did.resolve(didUri)`**
530
+ ## Advanced Usage
237
531
 
238
- Resolve a DID to its DID Document.
532
+ ### Unscoped DWN Access
239
533
 
240
- ```javascript
241
- const { didDocument } = await enbox.did.resolve(
242
- 'did:dht:qftx7z968xcpfy1a1diu75pg5meap3gdtg6ezagaw849wdh6oubo'
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