@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.
- package/dist/esm/dwn-registrar.js +87 -0
- package/dist/esm/dwn-registrar.js.map +1 -1
- package/dist/esm/http-dwn-rpc-client.js +145 -6
- package/dist/esm/http-dwn-rpc-client.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/json-rpc-socket.js +187 -35
- package/dist/esm/json-rpc-socket.js.map +1 -1
- package/dist/esm/json-rpc.js +13 -0
- package/dist/esm/json-rpc.js.map +1 -1
- package/dist/esm/provider-directory-types.js +2 -0
- package/dist/esm/provider-directory-types.js.map +1 -0
- package/dist/esm/rate-limit-error.js +14 -0
- package/dist/esm/rate-limit-error.js.map +1 -0
- package/dist/esm/rpc-client.js +1 -1
- package/dist/esm/rpc-client.js.map +1 -1
- package/dist/esm/web-socket-clients.js +102 -16
- package/dist/esm/web-socket-clients.js.map +1 -1
- package/dist/types/dwn-registrar.d.ts +29 -0
- package/dist/types/dwn-registrar.d.ts.map +1 -1
- package/dist/types/dwn-rpc-types.d.ts +54 -4
- package/dist/types/dwn-rpc-types.d.ts.map +1 -1
- package/dist/types/http-dwn-rpc-client.d.ts +24 -2
- package/dist/types/http-dwn-rpc-client.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/json-rpc-socket.d.ts +63 -0
- package/dist/types/json-rpc-socket.d.ts.map +1 -1
- package/dist/types/json-rpc.d.ts +7 -1
- package/dist/types/json-rpc.d.ts.map +1 -1
- package/dist/types/provider-directory-types.d.ts +40 -0
- package/dist/types/provider-directory-types.d.ts.map +1 -0
- package/dist/types/rate-limit-error.d.ts +12 -0
- package/dist/types/rate-limit-error.d.ts.map +1 -0
- package/dist/types/registration-types.d.ts +46 -2
- package/dist/types/registration-types.d.ts.map +1 -1
- package/dist/types/server-info-types.d.ts +26 -0
- package/dist/types/server-info-types.d.ts.map +1 -1
- package/dist/types/web-socket-clients.d.ts +12 -2
- package/dist/types/web-socket-clients.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/dwn-registrar.ts +106 -3
- package/src/dwn-rpc-types.ts +69 -4
- package/src/http-dwn-rpc-client.ts +182 -6
- package/src/index.ts +2 -0
- package/src/json-rpc-socket.ts +244 -36
- package/src/json-rpc.ts +17 -0
- package/src/provider-directory-types.ts +41 -0
- package/src/rate-limit-error.ts +16 -0
- package/src/registration-types.ts +50 -3
- package/src/rpc-client.ts +1 -1
- package/src/server-info-types.ts +27 -0
- 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
|
|
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
|
-
|
|
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();
|
package/src/server-info-types.ts
CHANGED
|
@@ -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 {
|
|
2
|
-
import type {
|
|
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,
|
|
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
|
|
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
|
|
32
|
-
|
|
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,
|
|
67
|
+
const { targetDid, message, subscription } = request;
|
|
41
68
|
|
|
42
|
-
if (
|
|
43
|
-
return WebSocketDwnRpcClient.subscriptionRequest(
|
|
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,
|
|
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
|
|
82
|
-
if (
|
|
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
|
|
91
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
}
|