@enbox/api 0.0.8 → 0.1.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 (48) hide show
  1. package/dist/browser.mjs +46 -53
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/define-protocol.js +37 -0
  4. package/dist/esm/define-protocol.js.map +1 -0
  5. package/dist/esm/dwn-api.js +54 -26
  6. package/dist/esm/dwn-api.js.map +1 -1
  7. package/dist/esm/index.js +4 -0
  8. package/dist/esm/index.js.map +1 -1
  9. package/dist/esm/live-query.js +153 -0
  10. package/dist/esm/live-query.js.map +1 -0
  11. package/dist/esm/protocol-types.js +8 -0
  12. package/dist/esm/protocol-types.js.map +1 -0
  13. package/dist/esm/record.js.map +1 -1
  14. package/dist/esm/typed-dwn-api.js +182 -0
  15. package/dist/esm/typed-dwn-api.js.map +1 -0
  16. package/dist/esm/web5.js +3 -2
  17. package/dist/esm/web5.js.map +1 -1
  18. package/dist/types/define-protocol.d.ts +37 -0
  19. package/dist/types/define-protocol.d.ts.map +1 -0
  20. package/dist/types/dwn-api.d.ts +32 -17
  21. package/dist/types/dwn-api.d.ts.map +1 -1
  22. package/dist/types/index.d.ts +4 -0
  23. package/dist/types/index.d.ts.map +1 -1
  24. package/dist/types/live-query.d.ts +136 -0
  25. package/dist/types/live-query.d.ts.map +1 -0
  26. package/dist/types/protocol-types.d.ts +95 -0
  27. package/dist/types/protocol-types.d.ts.map +1 -0
  28. package/dist/types/record.d.ts +1 -1
  29. package/dist/types/record.d.ts.map +1 -1
  30. package/dist/types/typed-dwn-api.d.ts +193 -0
  31. package/dist/types/typed-dwn-api.d.ts.map +1 -0
  32. package/dist/types/web5.d.ts.map +1 -1
  33. package/package.json +14 -17
  34. package/src/define-protocol.ts +48 -0
  35. package/src/dwn-api.ts +87 -49
  36. package/src/index.ts +4 -0
  37. package/src/live-query.ts +245 -0
  38. package/src/protocol-types.ts +171 -0
  39. package/src/record.ts +3 -3
  40. package/src/typed-dwn-api.ts +370 -0
  41. package/src/web5.ts +3 -2
  42. package/dist/browser.js +0 -2224
  43. package/dist/browser.js.map +0 -7
  44. package/dist/esm/subscription-util.js +0 -35
  45. package/dist/esm/subscription-util.js.map +0 -1
  46. package/dist/types/subscription-util.d.ts +0 -19
  47. package/dist/types/subscription-util.d.ts.map +0 -1
  48. package/src/subscription-util.ts +0 -44
package/src/dwn-api.ts CHANGED
@@ -8,9 +8,7 @@ import type {
8
8
  CreateGrantParams,
9
9
  CreateRequestParams,
10
10
  DwnMessage,
11
- DwnMessageParams
12
- ,
13
- DwnMessageSubscription,
11
+ DwnMessageParams,
14
12
  DwnPaginationCursor,
15
13
  DwnResponse,
16
14
  DwnResponseStatus,
@@ -26,12 +24,17 @@ import {
26
24
  import { isEmptyObject } from '@enbox/common';
27
25
  import { DwnInterface, getRecordAuthor } from '@enbox/agent';
28
26
 
27
+ import type { ProtocolDefinition } from '@enbox/dwn-sdk-js';
28
+
29
+ import type { SchemaMap, TypedProtocol } from './protocol-types.js';
30
+
29
31
  import { dataToBlob } from './utils.js';
32
+ import { LiveQuery } from './live-query.js';
30
33
  import { PermissionGrant } from './permission-grant.js';
31
34
  import { PermissionRequest } from './permission-request.js';
32
35
  import { Protocol } from './protocol.js';
33
36
  import { Record } from './record.js';
34
- import { SubscriptionUtil } from './subscription-util.js';
37
+ import { TypedDwnApi } from './typed-dwn-api.js';
35
38
 
36
39
  /**
37
40
  * Represents the request payload for fetching permission requests from a Decentralized Web Node (DWN).
@@ -221,39 +224,28 @@ export type RecordsReadResponse = DwnResponseStatus & {
221
224
  record: Record;
222
225
  };
223
226
 
224
- /** Subscription handler for Records */
225
- export type RecordsSubscriptionHandler = (record: Record) => void;
226
-
227
227
  /**
228
228
  * Represents a request to subscribe to records from a Decentralized Web Node (DWN).
229
229
  *
230
- * This request type is used to specify the target DWN from which records matching the subscription
231
- * criteria should be emitted. It's useful for being notified in real time when records are written, deleted or modified.
230
+ * Returns a {@link LiveQuery} that atomically provides an initial snapshot of
231
+ * matching records alongside a real-time stream of deduplicated, semantically-
232
+ * typed change events (`create`, `update`, `delete`).
232
233
  */
233
234
  export type RecordsSubscribeRequest = {
234
235
  /** Optional DID specifying the remote target DWN tenant to subscribe from. */
235
236
  from?: string;
236
237
 
237
- /** Records must be scoped to a specific protocol */
238
+ /** Records must be scoped to a specific protocol. */
238
239
  protocol?: string;
239
240
 
240
- /** The parameters for the subscription operation, detailing the criteria for the subscription filter */
241
+ /** The parameters for the subscription operation, detailing the criteria for the subscription filter. */
241
242
  message: Omit<DwnMessageParams[DwnInterface.RecordsSubscribe], 'signer'>;
242
-
243
- /** The handler to process the subscription events */
244
- subscriptionHandler: RecordsSubscriptionHandler;
245
-
246
- /** When true, indicates encryption is active (decryption happens on subsequent reads). */
247
- encryption?: boolean;
248
243
  };
249
244
 
250
- /** Encapsulates the response from a DWN RecordsSubscriptionRequest */
245
+ /** Encapsulates the response from a DWN RecordsSubscribeRequest. */
251
246
  export type RecordsSubscribeResponse = DwnResponseStatus & {
252
- /**
253
- * Represents the subscription that was created. Includes an ID and the close method to stop the subscription.
254
- *
255
- * */
256
- subscription?: DwnMessageSubscription;
247
+ /** The live query instance, or `undefined` if the request failed. */
248
+ liveQuery?: LiveQuery;
257
249
  };
258
250
 
259
251
  /**
@@ -331,6 +323,30 @@ export class DwnApi {
331
323
  this.permissionsApi = new AgentPermissionsApi({ agent: this.agent });
332
324
  }
333
325
 
326
+ /**
327
+ * Returns a {@link TypedDwnApi} scoped to the given typed protocol.
328
+ *
329
+ * The returned API provides path autocompletion, typed data payloads,
330
+ * and automatically injects the protocol URI and protocolPath into every
331
+ * DWN operation.
332
+ *
333
+ * @param protocol - A typed protocol created via `defineProtocol()`.
334
+ * @returns A `TypedDwnApi` instance bound to the given protocol.
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * const social = dwn.using(SocialGraphProtocol);
339
+ * const { record } = await social.write('friend', {
340
+ * data: { did: 'did:example:alice' },
341
+ * });
342
+ * ```
343
+ */
344
+ public using<D extends ProtocolDefinition, M extends SchemaMap>(
345
+ protocol: TypedProtocol<D, M>,
346
+ ): TypedDwnApi<D, M> {
347
+ return new TypedDwnApi<D, M>(this, protocol);
348
+ }
349
+
334
350
  /**
335
351
  * API to interact with Grants
336
352
  *
@@ -660,6 +676,7 @@ export class DwnApi {
660
676
 
661
677
  return { status };
662
678
  },
679
+
663
680
  /**
664
681
  * Query a single or multiple records based on the given filter
665
682
  */
@@ -846,38 +863,46 @@ export class DwnApi {
846
863
  },
847
864
 
848
865
  /**
849
- * Subscribes to records based on the given filter and emits events to the `subscriptionHandler`.
866
+ * Subscribe to records matching the given filter.
850
867
  *
851
- * @param request must include the `message` with the subscription filter and the `subscriptionHandler` to process the events.
852
- * @returns the subscription status and the subscription object used to close the subscription.
868
+ * Returns a {@link LiveQuery} that atomically provides an initial snapshot
869
+ * of matching records and a real-time stream of deduplicated, semantically-
870
+ * typed change events (`create`, `update`, `delete`).
853
871
  */
854
872
  subscribe: async (request: RecordsSubscribeRequest): Promise<RecordsSubscribeResponse> => {
873
+ // Build a DWN-level subscription handler that wraps raw RecordEvents
874
+ // into Record objects and feeds them into the LiveQuery.
875
+ let liveQuery: LiveQuery | undefined;
876
+
877
+ const remoteOrigin = request.from;
878
+ const protocolRole = request.message.protocolRole;
879
+
880
+ type RecordEvent = {
881
+ message: DwnMessage[DwnInterface.RecordsWrite];
882
+ initialWrite?: DwnMessage[DwnInterface.RecordsWrite];
883
+ };
884
+
885
+ const subscriptionHandler = async (event: RecordEvent): Promise<void> => {
886
+ const { message, initialWrite } = event;
887
+ const record = new Record(this.agent, {
888
+ ...message,
889
+ author : getRecordAuthor(message),
890
+ connectedDid : this.connectedDid,
891
+ remoteOrigin,
892
+ initialWrite,
893
+ protocolRole,
894
+ delegateDid : this.delegateDid,
895
+ }, this.permissionsApi);
896
+
897
+ liveQuery?.handleEvent(record);
898
+ };
899
+
855
900
  const agentRequest: ProcessDwnRequest<DwnInterface.RecordsSubscribe> = {
856
- /**
857
- * The `author` is the DID that will sign the message and must be the DID the Web5 app is
858
- * connected with and is authorized to access the signing private key of.
859
- */
860
901
  author : this.connectedDid,
861
902
  messageParams : request.message,
862
903
  messageType : DwnInterface.RecordsSubscribe,
863
- /**
864
- * The `target` is the DID of the DWN tenant under which the subscribe operation will be executed.
865
- * If `from` is provided, the subscribe operation will be executed on a remote DWN.
866
- * Otherwise, the local DWN will execute the subscribe operation.
867
- */
868
904
  target : request.from || this.connectedDid,
869
-
870
- /**
871
- * The handler to process the subscription events.
872
- */
873
- subscriptionHandler: SubscriptionUtil.recordSubscriptionHandler({
874
- agent : this.agent,
875
- connectedDid : this.connectedDid,
876
- delegateDid : this.delegateDid,
877
- permissionsApi : this.permissionsApi,
878
- protocolRole : request.message.protocolRole,
879
- request
880
- })
905
+ subscriptionHandler,
881
906
  };
882
907
 
883
908
  if (this.delegateDid) {
@@ -917,9 +942,22 @@ export class DwnApi {
917
942
  }
918
943
 
919
944
  const reply = agentResponse.reply;
920
- const { status, subscription } = reply;
945
+ const { status, subscription, entries = [] } = reply;
946
+
947
+ if (subscription) {
948
+ liveQuery = new LiveQuery({
949
+ agent : this.agent,
950
+ connectedDid : this.connectedDid,
951
+ delegateDid : this.delegateDid,
952
+ protocolRole,
953
+ remoteOrigin,
954
+ permissionsApi : this.permissionsApi,
955
+ initialEntries : entries,
956
+ subscription,
957
+ });
958
+ }
921
959
 
922
- return { status, subscription };
960
+ return { status, liveQuery };
923
961
  },
924
962
 
925
963
  /**
package/src/index.ts CHANGED
@@ -21,13 +21,17 @@
21
21
  * @packageDocumentation
22
22
  */
23
23
 
24
+ export * from './define-protocol.js';
24
25
  export * from './did-api.js';
25
26
  export * from './dwn-api.js';
26
27
  export * from './grant-revocation.js';
28
+ export * from './live-query.js';
27
29
  export * from './permission-grant.js';
28
30
  export * from './permission-request.js';
29
31
  export * from './protocol.js';
32
+ export * from './protocol-types.js';
30
33
  export * from './record.js';
34
+ export * from './typed-dwn-api.js';
31
35
  export * from './vc-api.js';
32
36
  export * from './web5.js';
33
37
 
@@ -0,0 +1,245 @@
1
+ import type { RecordsQueryReplyEntry } from '@enbox/dwn-sdk-js';
2
+ import type { DwnMessageSubscription, PermissionsApi, Web5Agent } from '@enbox/agent';
3
+
4
+ import { getRecordAuthor } from '@enbox/agent';
5
+
6
+ import { Record } from './record.js';
7
+
8
+ /**
9
+ * The type of change that occurred to a record.
10
+ */
11
+ export type RecordChangeType = 'create' | 'update' | 'delete';
12
+
13
+ /**
14
+ * Describes a change to a record in a {@link LiveQuery}.
15
+ */
16
+ export type RecordChange = {
17
+ /** Whether the record was created, updated, or deleted. */
18
+ type: RecordChangeType;
19
+
20
+ /** The record affected by the change. */
21
+ record: Record;
22
+ };
23
+
24
+ /**
25
+ * A `CustomEvent` subclass carrying a {@link RecordChange} as its `detail`.
26
+ *
27
+ * Dispatched on the {@link LiveQuery} `EventTarget` for both the specific
28
+ * change-type event (`create`, `update`, `delete`) and the catch-all `change`
29
+ * event.
30
+ */
31
+ export class RecordChangeEvent extends CustomEvent<RecordChange> {
32
+ constructor(change: RecordChange) {
33
+ super(change.type, { detail: change });
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Options for creating a {@link LiveQuery}.
39
+ * @internal — Constructed by `DwnApi.records.subscribe()`, not by end users.
40
+ */
41
+ export type LiveQueryOptions = {
42
+ /** The agent instance used to construct Record objects. */
43
+ agent: Web5Agent;
44
+
45
+ /** The DID of the connected user. */
46
+ connectedDid: string;
47
+
48
+ /** Optional delegate DID for permission-delegated access. */
49
+ delegateDid?: string;
50
+
51
+ /** Optional protocol role for role-authorized access. */
52
+ protocolRole?: string;
53
+
54
+ /** Optional remote DWN origin if subscribing to a remote DWN. */
55
+ remoteOrigin?: string;
56
+
57
+ /** The permissions API instance for constructing Record objects. */
58
+ permissionsApi?: PermissionsApi;
59
+
60
+ /** The initial snapshot entries from the subscribe reply. */
61
+ initialEntries: RecordsQueryReplyEntry[];
62
+
63
+ /** The underlying DWN subscription handle. */
64
+ subscription: DwnMessageSubscription;
65
+ };
66
+
67
+ /**
68
+ * A live query that combines an initial snapshot of matching records with a
69
+ * real-time stream of deduplicated, semantically-typed change events.
70
+ *
71
+ * `LiveQuery` extends `EventTarget` so that standard `addEventListener` /
72
+ * `removeEventListener` work out of the box. For convenience, the typed
73
+ * {@link LiveQuery.on | `.on()`} method provides a cleaner API that returns an
74
+ * unsubscribe function.
75
+ *
76
+ * ### Events
77
+ *
78
+ * | Event name | `detail` type | Description |
79
+ * |---|---|---|
80
+ * | `create` | {@link RecordChange} | A new record was written |
81
+ * | `update` | {@link RecordChange} | An existing record was updated |
82
+ * | `delete` | {@link RecordChange} | A record was deleted |
83
+ * | `change` | {@link RecordChange} | Catch-all for any of the above |
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * const { liveQuery } = await dwn.records.subscribe({
88
+ * message: {
89
+ * filter: {
90
+ * protocol : chatProtocol.protocol,
91
+ * protocolPath : 'thread/message',
92
+ * }
93
+ * }
94
+ * });
95
+ *
96
+ * // Initial state
97
+ * for (const record of liveQuery.records) {
98
+ * renderMessage(record);
99
+ * }
100
+ *
101
+ * // Real-time changes
102
+ * liveQuery.on('create', (record) => appendMessage(record));
103
+ * liveQuery.on('update', (record) => refreshMessage(record));
104
+ * liveQuery.on('delete', (record) => removeMessage(record));
105
+ *
106
+ * // Or use the catch-all
107
+ * liveQuery.on('change', ({ type, record }) => {
108
+ * console.log(`${type}: ${record.id}`);
109
+ * });
110
+ *
111
+ * // Cleanup
112
+ * await liveQuery.close();
113
+ * ```
114
+ */
115
+ export class LiveQuery extends EventTarget {
116
+ /** The initial snapshot of matching records. */
117
+ readonly records: Record[];
118
+
119
+ /** The underlying DWN subscription handle. */
120
+ private _subscription: DwnMessageSubscription;
121
+
122
+ /** Tracks known record states for dedup and change-type classification. */
123
+ private _knownRecords: Map<string, string>;
124
+
125
+ /** Whether the live query has been closed. */
126
+ private _closed = false;
127
+
128
+ constructor(options: LiveQueryOptions) {
129
+ super();
130
+
131
+ const {
132
+ agent,
133
+ connectedDid,
134
+ delegateDid,
135
+ protocolRole,
136
+ remoteOrigin,
137
+ permissionsApi,
138
+ initialEntries,
139
+ subscription,
140
+ } = options;
141
+
142
+ this._subscription = subscription;
143
+
144
+ // Build Record objects from the initial snapshot entries (same logic as records.query()).
145
+ this.records = initialEntries.map((entry) => new Record(agent, {
146
+ author: getRecordAuthor(entry),
147
+ connectedDid,
148
+ remoteOrigin,
149
+ delegateDid,
150
+ protocolRole,
151
+ ...entry,
152
+ }, permissionsApi));
153
+
154
+ // Seed the known-records map with recordId -> messageTimestamp for dedup.
155
+ this._knownRecords = new Map();
156
+ for (const record of this.records) {
157
+ this._knownRecords.set(record.id, record.dateModified);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Process an incoming live event from the DWN subscription.
163
+ * Deduplicates against the initial snapshot and classifies the change type.
164
+ *
165
+ * @internal — Called by `DwnApi.records.subscribe()` when wiring up the subscription handler.
166
+ */
167
+ public handleEvent(record: Record): void {
168
+ if (this._closed) {
169
+ return;
170
+ }
171
+
172
+ let changeType: RecordChangeType;
173
+
174
+ if (record.deleted) {
175
+ changeType = 'delete';
176
+ this._knownRecords.delete(record.id);
177
+ } else {
178
+ const knownTimestamp = this._knownRecords.get(record.id);
179
+
180
+ if (knownTimestamp !== undefined) {
181
+ // We've seen this recordId before (either from snapshot or a prior event).
182
+ if (record.dateModified <= knownTimestamp) {
183
+ // Duplicate or stale event from the overlap window — skip.
184
+ return;
185
+ }
186
+ changeType = 'update';
187
+ } else {
188
+ changeType = 'create';
189
+ }
190
+
191
+ // Update the known state.
192
+ this._knownRecords.set(record.id, record.dateModified);
193
+ }
194
+
195
+ const change: RecordChange = { type: changeType, record };
196
+
197
+ // Dispatch the specific event (create/update/delete).
198
+ this.dispatchEvent(new RecordChangeEvent(change));
199
+
200
+ // Dispatch the catch-all change event.
201
+ this.dispatchEvent(new CustomEvent('change', { detail: change }));
202
+ }
203
+
204
+ /**
205
+ * Register a typed event handler. Returns an unsubscribe function.
206
+ *
207
+ * @param event - The event type to listen for.
208
+ * @param handler - The handler function.
209
+ * @returns A function that removes the handler when called.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * const off = live.on('create', (record) => console.log(record.id));
214
+ * off(); // stop listening
215
+ * ```
216
+ */
217
+ on(event: 'change', handler: (change: RecordChange) => void): () => void;
218
+ on(event: 'create', handler: (record: Record) => void): () => void;
219
+ on(event: 'update', handler: (record: Record) => void): () => void;
220
+ on(event: 'delete', handler: (record: Record) => void): () => void;
221
+ on(event: 'change' | 'create' | 'update' | 'delete', handler: ((change: RecordChange) => void) | ((record: Record) => void)): () => void {
222
+ const wrapper = (e: Event): void => {
223
+ const detail = (e as CustomEvent<RecordChange>).detail;
224
+ if (event === 'change') {
225
+ (handler as (change: RecordChange) => void)(detail);
226
+ } else {
227
+ (handler as (record: Record) => void)(detail.record);
228
+ }
229
+ };
230
+
231
+ this.addEventListener(event, wrapper);
232
+ return (): void => { this.removeEventListener(event, wrapper); };
233
+ }
234
+
235
+ /**
236
+ * Close the underlying subscription and stop dispatching events.
237
+ */
238
+ async close(): Promise<void> {
239
+ if (this._closed) {
240
+ return;
241
+ }
242
+ this._closed = true;
243
+ await this._subscription.close();
244
+ }
245
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Type-level utilities for extracting typed paths, type names, schemas,
3
+ * data formats, and tag shapes from a {@link ProtocolDefinition}.
4
+ *
5
+ * These types are purely compile-time — they produce no runtime code.
6
+ */
7
+
8
+ import type { ProtocolDefinition, ProtocolRuleSet, ProtocolTagsDefinition, ProtocolType } from '@enbox/dwn-sdk-js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Path extraction
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Recursively extracts all valid protocol path strings from a `ProtocolRuleSet`.
16
+ *
17
+ * Given a structure like `{ foo: { bar: { ... } } }`, this produces
18
+ * `'foo' | 'foo/bar'`. Directive keys (starting with `$`) are excluded.
19
+ */
20
+ export type RuleSetPaths<R, Prefix extends string = ''> = {
21
+ [K in Extract<keyof R, string>]: K extends `$${string}`
22
+ ? never
23
+ : R[K] extends ProtocolRuleSet
24
+ ? `${Prefix}${K}` | RuleSetPaths<R[K], `${Prefix}${K}/`>
25
+ : never;
26
+ }[Extract<keyof R, string>];
27
+
28
+ /**
29
+ * All valid protocol path strings for a given `ProtocolDefinition`.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * type Paths = ProtocolPaths<typeof myDef>;
34
+ * // 'friend' | 'friend/message' | 'group' | 'group/member'
35
+ * ```
36
+ */
37
+ export type ProtocolPaths<D extends ProtocolDefinition> = RuleSetPaths<D['structure']>;
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Type-name extraction
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Extracts the last segment (type name) from a protocol path string.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * type T = TypeNameAtPath<'group/member'>; // 'member'
49
+ * ```
50
+ */
51
+ export type TypeNameAtPath<Path extends string> =
52
+ Path extends `${string}/${infer Rest}`
53
+ ? TypeNameAtPath<Rest>
54
+ : Path;
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Schema & data-format lookup
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * The type names declared in the `types` map of a `ProtocolDefinition`.
62
+ */
63
+ export type TypeNames<D extends ProtocolDefinition> = Extract<keyof D['types'], string>;
64
+
65
+ /**
66
+ * Looks up the `schema` URI for a given type name in the protocol definition.
67
+ * Returns `undefined` if the type does not declare a schema.
68
+ */
69
+ export type SchemaForType<D extends ProtocolDefinition, TypeName extends string> =
70
+ TypeName extends keyof D['types']
71
+ ? D['types'][TypeName] extends ProtocolType
72
+ ? D['types'][TypeName]['schema']
73
+ : undefined
74
+ : undefined;
75
+
76
+ /**
77
+ * Looks up the `dataFormats` array for a given type name in the protocol definition.
78
+ * Returns `undefined` if the type does not declare dataFormats.
79
+ */
80
+ export type DataFormatsForType<D extends ProtocolDefinition, TypeName extends string> =
81
+ TypeName extends keyof D['types']
82
+ ? D['types'][TypeName] extends ProtocolType
83
+ ? D['types'][TypeName]['dataFormats']
84
+ : undefined
85
+ : undefined;
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Tag extraction helpers
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Navigates a `ProtocolRuleSet` tree to the node at the given slash-delimited path.
93
+ */
94
+ type RuleSetAtPath<R, Path extends string> =
95
+ Path extends `${infer Head}/${infer Tail}`
96
+ ? Head extends keyof R
97
+ ? R[Head] extends ProtocolRuleSet
98
+ ? RuleSetAtPath<R[Head], Tail>
99
+ : never
100
+ : never
101
+ : Path extends keyof R
102
+ ? R[Path] extends ProtocolRuleSet
103
+ ? R[Path]
104
+ : never
105
+ : never;
106
+
107
+ /**
108
+ * Extracts the `$tags` definition for a given protocol path.
109
+ * Returns `never` if the path does not declare `$tags`.
110
+ */
111
+ export type TagsAtPath<D extends ProtocolDefinition, Path extends string> =
112
+ RuleSetAtPath<D['structure'], Path> extends infer RS
113
+ ? RS extends ProtocolRuleSet
114
+ ? RS['$tags'] extends ProtocolTagsDefinition
115
+ ? RS['$tags']
116
+ : never
117
+ : never
118
+ : never;
119
+
120
+ /**
121
+ * Extracts the user-defined tag keys (excluding `$`-prefixed meta-keys)
122
+ * from a `ProtocolTagsDefinition`.
123
+ */
124
+ export type TagKeys<Tags extends ProtocolTagsDefinition> = Exclude<
125
+ Extract<keyof Tags, string>,
126
+ `$${string}`
127
+ >;
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Schema map — associates TypeScript data types with protocol type names
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * A mapping from protocol type names to their TypeScript data shapes.
135
+ *
136
+ * Used as a type parameter to `defineProtocol()` and `TypedDwnApi` so that
137
+ * the protocol definition JSON stays JSON-compatible while TypeScript types
138
+ * are tracked separately.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * type MySchemaMap = {
143
+ * profile : { displayName: string; bio?: string };
144
+ * avatar : Blob;
145
+ * };
146
+ * ```
147
+ */
148
+ export type SchemaMap = Record<string, unknown>;
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Typed protocol — output of defineProtocol()
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /**
155
+ * The return type of `defineProtocol()`. Bundles the raw protocol definition
156
+ * with its inferred path strings and schema type map for downstream use
157
+ * by `TypedDwnApi`.
158
+ */
159
+ export type TypedProtocol<
160
+ D extends ProtocolDefinition = ProtocolDefinition,
161
+ M extends SchemaMap = SchemaMap,
162
+ > = {
163
+ /** The raw DWN protocol definition (JSON-compatible). */
164
+ readonly definition: D;
165
+
166
+ /**
167
+ * Phantom property carrying the schema map type. Not present at runtime;
168
+ * used only by TypeScript to thread the generic through.
169
+ */
170
+ readonly _schemaMap?: M;
171
+ };
package/src/record.ts CHANGED
@@ -445,7 +445,7 @@ export class Record implements RecordModel {
445
445
  get data(): {
446
446
  blob: () => Promise<Blob>;
447
447
  bytes: () => Promise<Uint8Array>;
448
- json: () => Promise<any>;
448
+ json: <T = unknown>() => Promise<T>;
449
449
  text: () => Promise<string>;
450
450
  stream: () => Promise<ReadableStream>;
451
451
  then: (
@@ -489,8 +489,8 @@ export class Record implements RecordModel {
489
489
  *
490
490
  * @beta
491
491
  */
492
- async json(): Promise<any> {
493
- return await Stream.consumeToJson({ readableStream: await this.stream() });
492
+ async json<T = unknown>(): Promise<T> {
493
+ return await Stream.consumeToJson({ readableStream: await this.stream() }) as T;
494
494
  },
495
495
 
496
496
  /**