@enbox/dwn-clients 0.0.5 → 0.0.7

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 (53) hide show
  1. package/dist/esm/dwn-registrar.js +87 -0
  2. package/dist/esm/dwn-registrar.js.map +1 -1
  3. package/dist/esm/http-dwn-rpc-client.js +145 -6
  4. package/dist/esm/http-dwn-rpc-client.js.map +1 -1
  5. package/dist/esm/index.js +2 -0
  6. package/dist/esm/index.js.map +1 -1
  7. package/dist/esm/json-rpc-socket.js +187 -35
  8. package/dist/esm/json-rpc-socket.js.map +1 -1
  9. package/dist/esm/json-rpc.js +13 -0
  10. package/dist/esm/json-rpc.js.map +1 -1
  11. package/dist/esm/provider-directory-types.js +2 -0
  12. package/dist/esm/provider-directory-types.js.map +1 -0
  13. package/dist/esm/rate-limit-error.js +14 -0
  14. package/dist/esm/rate-limit-error.js.map +1 -0
  15. package/dist/esm/rpc-client.js +1 -1
  16. package/dist/esm/rpc-client.js.map +1 -1
  17. package/dist/esm/web-socket-clients.js +102 -16
  18. package/dist/esm/web-socket-clients.js.map +1 -1
  19. package/dist/types/dwn-registrar.d.ts +29 -0
  20. package/dist/types/dwn-registrar.d.ts.map +1 -1
  21. package/dist/types/dwn-rpc-types.d.ts +54 -4
  22. package/dist/types/dwn-rpc-types.d.ts.map +1 -1
  23. package/dist/types/http-dwn-rpc-client.d.ts +24 -2
  24. package/dist/types/http-dwn-rpc-client.d.ts.map +1 -1
  25. package/dist/types/index.d.ts +2 -0
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/types/json-rpc-socket.d.ts +63 -0
  28. package/dist/types/json-rpc-socket.d.ts.map +1 -1
  29. package/dist/types/json-rpc.d.ts +7 -1
  30. package/dist/types/json-rpc.d.ts.map +1 -1
  31. package/dist/types/provider-directory-types.d.ts +40 -0
  32. package/dist/types/provider-directory-types.d.ts.map +1 -0
  33. package/dist/types/rate-limit-error.d.ts +12 -0
  34. package/dist/types/rate-limit-error.d.ts.map +1 -0
  35. package/dist/types/registration-types.d.ts +46 -2
  36. package/dist/types/registration-types.d.ts.map +1 -1
  37. package/dist/types/server-info-types.d.ts +26 -0
  38. package/dist/types/server-info-types.d.ts.map +1 -1
  39. package/dist/types/web-socket-clients.d.ts +12 -2
  40. package/dist/types/web-socket-clients.d.ts.map +1 -1
  41. package/package.json +4 -4
  42. package/src/dwn-registrar.ts +106 -3
  43. package/src/dwn-rpc-types.ts +69 -4
  44. package/src/http-dwn-rpc-client.ts +182 -6
  45. package/src/index.ts +2 -0
  46. package/src/json-rpc-socket.ts +244 -36
  47. package/src/json-rpc.ts +17 -0
  48. package/src/provider-directory-types.ts +41 -0
  49. package/src/rate-limit-error.ts +16 -0
  50. package/src/registration-types.ts +50 -3
  51. package/src/rpc-client.ts +1 -1
  52. package/src/server-info-types.ts +27 -0
  53. package/src/web-socket-clients.ts +156 -20
@@ -11,16 +11,63 @@ export type ProofOfWorkChallengeModel = {
11
11
  */
12
12
  export type RegistrationData = {
13
13
  did: string;
14
- termsOfServiceHash: string;
14
+ termsOfServiceHash?: string;
15
15
  };
16
16
 
17
17
  /**
18
18
  * Full registration request body for `POST /registration`.
19
19
  */
20
20
  export type RegistrationRequest = {
21
- proofOfWork: {
21
+ proofOfWork?: {
22
22
  challengeNonce: string;
23
23
  responseNonce: string;
24
24
  },
25
- registrationData: RegistrationData
25
+ /**
26
+ * Provider-auth-v0 credentials. Present when the server requires
27
+ * `'provider-auth-v0'` registration.
28
+ */
29
+ providerAuth?: {
30
+ /** The registration token obtained from the token exchange endpoint. */
31
+ registrationToken: string;
32
+ },
33
+ registrationData: RegistrationData,
34
+ };
35
+
36
+ /**
37
+ * Request body for `POST {tokenUrl}` — exchanges an authorization code for a
38
+ * registration token. Sent by the wallet to the provider's auth service.
39
+ */
40
+ export type TokenExchangeRequest = {
41
+ /** Grant type identifier. */
42
+ grantType : 'authorization_code';
43
+ /** The authorization code received from the provider's redirect. */
44
+ code : string;
45
+ /** The redirect URI used in the authorization request (must match exactly). */
46
+ redirectUri : string;
47
+ };
48
+
49
+ /**
50
+ * Response body from `POST {tokenUrl}` or `POST {refreshUrl}` — contains the
51
+ * registration token and optional refresh token.
52
+ */
53
+ export type TokenExchangeResponse = {
54
+ /** Opaque registration token for use with `POST /registration`. */
55
+ registrationToken : string;
56
+ /** Opaque refresh token for obtaining new registration tokens. */
57
+ refreshToken? : string;
58
+ /** Token lifetime in seconds. If absent, the token does not expire. */
59
+ expiresIn? : number;
60
+ /** Token type hint (e.g. `'bearer'`). */
61
+ tokenType : string;
62
+ };
63
+
64
+ /**
65
+ * Request body for `POST {refreshUrl}` — refreshes an expired registration
66
+ * token.
67
+ */
68
+ export type TokenRefreshRequest = {
69
+ /** Grant type identifier. */
70
+ grantType : 'refresh_token';
71
+ /** The refresh token obtained from a prior token exchange. */
72
+ refreshToken : string;
26
73
  };
package/src/rpc-client.ts CHANGED
@@ -128,7 +128,7 @@ export class HttpWeb5RpcClient extends HttpDwnRpcClient implements Web5Rpc {
128
128
  let jsonRpcResponse: JsonRpcResponse;
129
129
 
130
130
  try {
131
- const response = await fetch(httpRequest);
131
+ const response = await fetch(httpRequest, { signal: AbortSignal.timeout(30_000) });
132
132
 
133
133
  if (response.ok) {
134
134
  jsonRpcResponse = await response.json();
@@ -1,8 +1,35 @@
1
1
  import type { KeyValueStore } from '@enbox/common';
2
2
 
3
+ /**
4
+ * Configuration for provider-auth-v0 registration.
5
+ * Present in {@link ServerInfo} when `'provider-auth-v0'` is listed in
6
+ * {@link ServerInfo.registrationRequirements | registrationRequirements}.
7
+ */
8
+ export type ProviderAuthInfo = {
9
+ /** URL to redirect user for authentication/signup/payment. Can be on the DWN server or an external domain. */
10
+ authorizeUrl : string;
11
+ /** URL where the wallet exchanges an authorization code for a registration token. */
12
+ tokenUrl : string;
13
+ /** URL to refresh an expired registration token. If absent, tokens do not support refresh. */
14
+ refreshUrl? : string;
15
+ /** URL for user-facing account management dashboard. If absent, no management UI is available. */
16
+ managementUrl? : string;
17
+ };
18
+
3
19
  export type ServerInfo = {
4
20
  /** the maximum file size the user can request to store */
5
21
  maxFileSize: number,
22
+ /**
23
+ * Maximum number of unacknowledged subscription events the server will send
24
+ * before pausing delivery. Clients ****MUST**** send `rpc.ack` to advance the
25
+ * window. When absent, the server does not enforce backpressure.
26
+ */
27
+ maxInFlight?: number,
28
+ /**
29
+ * Provider-auth-v0 configuration. Present when `'provider-auth-v0'` is
30
+ * included in {@link registrationRequirements}.
31
+ */
32
+ providerAuth?: ProviderAuthInfo,
6
33
  /**
7
34
  * an array of strings representing the server's registration requirements.
8
35
  *
@@ -1,14 +1,42 @@
1
- import type { JsonRpcSocketOptions } from './json-rpc-socket.js';
2
- import type { DwnRpc, DwnRpcRequest, DwnRpcResponse, DwnSubscriptionHandler } from './dwn-rpc-types.js';
3
- import type { GenericMessage, MessageSubscription, UnionMessageReply } from '@enbox/dwn-sdk-js';
1
+ import type { DwnRpc, DwnRpcRequest, DwnRpcResponse, DwnSubscriptionHandler, ResubscribeFactory } from './dwn-rpc-types.js';
2
+ import type { GenericMessage, MessageSubscription, SubscriptionMessage, UnionMessageReply } from '@enbox/dwn-sdk-js';
4
3
 
5
4
  import { CryptoUtils } from '@enbox/crypto';
6
5
  import { JsonRpcSocket } from './json-rpc-socket.js';
7
- import { createJsonRpcRequest, createJsonRpcSubscriptionRequest } from './json-rpc.js';
6
+ import { createJsonRpcAck, createJsonRpcRequest, createJsonRpcSubscriptionRequest } from './json-rpc.js';
7
+
8
+ /**
9
+ * Metadata for a tracked subscription, including everything needed to
10
+ * resubscribe after a reconnection.
11
+ */
12
+ interface TrackedSubscription {
13
+ /** The DWN `MessageSubscription` handle. */
14
+ subscription: MessageSubscription;
15
+
16
+ /** The target DID for the subscription. */
17
+ target: string;
18
+
19
+ /** The original DWN subscribe message (fallback when no resubscribeFactory). */
20
+ message: GenericMessage;
21
+
22
+ /** The application-level subscription handler. */
23
+ handler: DwnSubscriptionHandler;
24
+
25
+ /**
26
+ * Factory that reconstructs and re-signs the subscribe message with a cursor.
27
+ * When present, used instead of the original `message` during resubscription.
28
+ */
29
+ resubscribeFactory?: ResubscribeFactory;
30
+
31
+ /** The cursor from the most recently received subscription event. */
32
+ lastCursor?: string;
33
+ }
8
34
 
9
35
  interface SocketConnection {
10
36
  socket: JsonRpcSocket;
11
- subscriptions: Map<string, MessageSubscription>;
37
+ subscriptions: Map<string, TrackedSubscription>;
38
+ /** The original URL used to create this connection. */
39
+ url: string;
12
40
  }
13
41
 
14
42
  export class WebSocketDwnRpcClient implements DwnRpc {
@@ -16,7 +44,7 @@ export class WebSocketDwnRpcClient implements DwnRpc {
16
44
  // a map of dwn host to WebSocket connection
17
45
  private static connections = new Map<string, SocketConnection>();
18
46
 
19
- async sendDwnRequest(request: DwnRpcRequest, jsonRpcSocketOptions?: JsonRpcSocketOptions): Promise<DwnRpcResponse> {
47
+ async sendDwnRequest(request: DwnRpcRequest): Promise<DwnRpcResponse> {
20
48
 
21
49
  // validate that the dwn URL provided is a valid WebSocket URL
22
50
  const url = new URL(request.dwnUrl);
@@ -28,24 +56,62 @@ export class WebSocketDwnRpcClient implements DwnRpc {
28
56
  const hasConnection = WebSocketDwnRpcClient.connections.has(url.host);
29
57
  if (!hasConnection) {
30
58
  try {
31
- const socket = await JsonRpcSocket.connect(url.toString(), jsonRpcSocketOptions);
32
- const subscriptions = new Map();
33
- WebSocketDwnRpcClient.connections.set(url.host, { socket, subscriptions });
59
+ const connection = await WebSocketDwnRpcClient.createConnection(url);
60
+ WebSocketDwnRpcClient.connections.set(url.host, connection);
34
61
  } catch (error) {
35
62
  throw new Error(`Error connecting to ${url.host}: ${(error as Error).message}`);
36
63
  }
37
64
  }
38
65
 
39
66
  const connection = WebSocketDwnRpcClient.connections.get(url.host)!;
40
- const { targetDid, message, subscriptionHandler } = request;
67
+ const { targetDid, message, subscription } = request;
41
68
 
42
- if (subscriptionHandler) {
43
- return WebSocketDwnRpcClient.subscriptionRequest(connection, targetDid, message, subscriptionHandler);
69
+ if (subscription) {
70
+ return WebSocketDwnRpcClient.subscriptionRequest(
71
+ connection, targetDid, message, subscription.handler, subscription.resubscribeFactory
72
+ );
44
73
  }
45
74
 
46
75
  return WebSocketDwnRpcClient.processMessage(connection, targetDid, message);
47
76
  }
48
77
 
78
+ /**
79
+ * Creates a new `SocketConnection` with lifecycle wiring for reconnection.
80
+ */
81
+ private static async createConnection(url: URL): Promise<SocketConnection> {
82
+ const host = url.host;
83
+ const subscriptions = new Map<string, TrackedSubscription>();
84
+
85
+ const socket = await JsonRpcSocket.connect(url.toString(), {
86
+ onclose: (): void => {
87
+ // Remove the stale connection from the map so new requests create a fresh one.
88
+ WebSocketDwnRpcClient.connections.delete(host);
89
+
90
+ // Notify all subscription handlers of disconnection.
91
+ for (const tracked of subscriptions.values()) {
92
+ tracked.handler({ type: 'disconnected' });
93
+ }
94
+ },
95
+
96
+ onreconnecting: (attempt: number): void => {
97
+ for (const tracked of subscriptions.values()) {
98
+ tracked.handler({ type: 'reconnecting', attempt });
99
+ }
100
+ },
101
+
102
+ onreconnected: (): void => {
103
+ // Re-register this connection in the map (it was deleted on close).
104
+ const conn = { socket, subscriptions, url: url.toString() };
105
+ WebSocketDwnRpcClient.connections.set(host, conn);
106
+
107
+ // Resubscribe all tracked subscriptions with their last known cursor.
108
+ WebSocketDwnRpcClient.resubscribeAll(conn);
109
+ },
110
+ });
111
+
112
+ return { socket, subscriptions, url: url.toString() };
113
+ }
114
+
49
115
  private static async processMessage(
50
116
  connection: SocketConnection, target: string, message: GenericMessage
51
117
  ): Promise<DwnRpcResponse> {
@@ -64,7 +130,11 @@ export class WebSocketDwnRpcClient implements DwnRpc {
64
130
  }
65
131
 
66
132
  private static async subscriptionRequest(
67
- connection: SocketConnection, target:string, message: GenericMessage, messageHandler: DwnSubscriptionHandler
133
+ connection: SocketConnection,
134
+ target: string,
135
+ message: GenericMessage,
136
+ handler: DwnSubscriptionHandler,
137
+ resubscribeFactory?: ResubscribeFactory,
68
138
  ): Promise<DwnRpcResponse> {
69
139
  const requestId = CryptoUtils.randomUuid();
70
140
  const subscriptionId = CryptoUtils.randomUuid();
@@ -78,17 +148,28 @@ export class WebSocketDwnRpcClient implements DwnRpc {
78
148
  if (error) {
79
149
 
80
150
  // if there is an error, close the subscription and delete it from the connection
81
- const subscription = subscriptions.get(subscriptionId);
82
- if (subscription) {
83
- subscription.close();
151
+ const tracked = subscriptions.get(subscriptionId);
152
+ if (tracked) {
153
+ tracked.subscription.close();
84
154
  }
85
155
 
86
156
  subscriptions.delete(subscriptionId);
87
157
  return;
88
158
  }
89
159
 
90
- const { event } = result;
91
- messageHandler(event);
160
+ const subscriptionMessage = result.subscription as SubscriptionMessage;
161
+ handler(subscriptionMessage);
162
+
163
+ // Track the latest cursor for reconnection.
164
+ if ('cursor' in subscriptionMessage && subscriptionMessage.cursor) {
165
+ const tracked = subscriptions.get(subscriptionId);
166
+ if (tracked) {
167
+ tracked.lastCursor = subscriptionMessage.cursor;
168
+ }
169
+
170
+ // Send rpc.ack to advance the server's flow-control window.
171
+ socket.send(createJsonRpcAck(subscriptionId, subscriptionMessage.cursor));
172
+ }
92
173
  });
93
174
 
94
175
  const { error, result } = response;
@@ -98,10 +179,65 @@ export class WebSocketDwnRpcClient implements DwnRpc {
98
179
 
99
180
  const { reply } = result as { reply: UnionMessageReply };
100
181
  if (reply.subscription && close) {
101
- subscriptions.set(subscriptionId, { ...reply.subscription, close });
102
- reply.subscription.close = close;
182
+ const wrappedClose = async (): Promise<void> => {
183
+ subscriptions.delete(subscriptionId);
184
+ await close();
185
+ };
186
+
187
+ const tracked: TrackedSubscription = {
188
+ subscription: { ...reply.subscription, close: wrappedClose },
189
+ target,
190
+ message,
191
+ handler,
192
+ resubscribeFactory,
193
+ };
194
+
195
+ subscriptions.set(subscriptionId, tracked);
196
+ reply.subscription.close = wrappedClose;
103
197
  }
104
198
 
105
199
  return reply;
106
200
  }
201
+
202
+ /**
203
+ * Resubscribes all tracked subscriptions on a reconnected socket.
204
+ * Uses the `resubscribeFactory` (if provided) to construct a properly signed
205
+ * message with the last known cursor. Falls back to the original message
206
+ * for anonymous/unsigned subscriptions.
207
+ */
208
+ private static async resubscribeAll(connection: SocketConnection): Promise<void> {
209
+ // Snapshot the current subscriptions — resubscription will re-populate the map.
210
+ const entries = [...connection.subscriptions.entries()];
211
+ connection.subscriptions.clear();
212
+
213
+ for (const [, tracked] of entries) {
214
+ try {
215
+ let resumeMessage: GenericMessage;
216
+
217
+ if (tracked.resubscribeFactory) {
218
+ // Reconstruct and re-sign the message with the cursor.
219
+ resumeMessage = await tracked.resubscribeFactory(tracked.lastCursor);
220
+ } else {
221
+ // No factory — reuse the original message as-is.
222
+ // This only works for anonymous (unsigned) subscriptions.
223
+ resumeMessage = tracked.message;
224
+ }
225
+
226
+ await WebSocketDwnRpcClient.subscriptionRequest(
227
+ connection,
228
+ tracked.target,
229
+ resumeMessage,
230
+ tracked.handler,
231
+ tracked.resubscribeFactory,
232
+ );
233
+
234
+ // Notify the handler that reconnection is complete for this subscription.
235
+ tracked.handler({ type: 'reconnected' });
236
+ } catch {
237
+ // If resubscription fails for one subscription, continue with the rest.
238
+ // The subscription is effectively lost — the handler was already
239
+ // notified of disconnection.
240
+ }
241
+ }
242
+ }
107
243
  }