@enbox/agent 0.3.1 → 0.4.0
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/browser.mjs +12 -30
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/connect.js +20 -24
- package/dist/esm/connect.js.map +1 -1
- package/dist/esm/dwn-api.js +149 -22
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-discovery-file.js +1 -1
- package/dist/esm/dwn-discovery-payload.js +20 -21
- package/dist/esm/dwn-discovery-payload.js.map +1 -1
- package/dist/esm/dwn-key-delivery.js.map +1 -1
- package/dist/esm/{oidc.js → enbox-connect-protocol.js} +236 -248
- package/dist/esm/enbox-connect-protocol.js.map +1 -0
- package/dist/esm/enbox-user-agent.js +18 -5
- package/dist/esm/enbox-user-agent.js.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/local-dwn.js +21 -51
- package/dist/esm/local-dwn.js.map +1 -1
- package/dist/esm/permissions-api.js.map +1 -1
- package/dist/esm/store-data.js.map +1 -1
- package/dist/esm/sync-engine-level.js +1 -1
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +1 -1
- package/dist/esm/sync-messages.js.map +1 -1
- package/dist/types/connect.d.ts +15 -19
- package/dist/types/connect.d.ts.map +1 -1
- package/dist/types/dwn-api.d.ts +46 -6
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-discovery-file.d.ts +1 -1
- package/dist/types/dwn-discovery-payload.d.ts +18 -19
- package/dist/types/dwn-discovery-payload.d.ts.map +1 -1
- package/dist/types/enbox-connect-protocol.d.ts +220 -0
- package/dist/types/enbox-connect-protocol.d.ts.map +1 -0
- package/dist/types/enbox-user-agent.d.ts +10 -1
- package/dist/types/enbox-user-agent.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/local-dwn.d.ts +16 -32
- package/dist/types/local-dwn.d.ts.map +1 -1
- package/package.json +9 -11
- package/src/connect.ts +38 -47
- package/src/dwn-api.ts +175 -29
- package/src/dwn-discovery-file.ts +1 -1
- package/src/dwn-discovery-payload.ts +23 -24
- package/src/dwn-key-delivery.ts +1 -1
- package/src/enbox-connect-protocol.ts +778 -0
- package/src/enbox-user-agent.ts +27 -4
- package/src/index.ts +4 -2
- package/src/local-dwn.ts +22 -53
- package/src/permissions-api.ts +3 -3
- package/src/store-data.ts +1 -1
- package/src/sync-engine-level.ts +1 -1
- package/src/sync-messages.ts +1 -1
- package/dist/esm/oidc.js.map +0 -1
- package/dist/types/oidc.d.ts +0 -250
- package/dist/types/oidc.d.ts.map +0 -1
- package/src/oidc.ts +0 -864
|
@@ -7,37 +7,20 @@
|
|
|
7
7
|
* 2. **Discovery file** (`~/.enbox/dwn.json`) — written by `electrobun-dwn`
|
|
8
8
|
* on startup. Fast filesystem read, no network. Available for CLI and
|
|
9
9
|
* native apps; skipped in browsers.
|
|
10
|
-
* 3. **
|
|
11
|
-
*
|
|
10
|
+
* 3. **Injected endpoint** — in browsers, the `dwn://connect` redirect
|
|
11
|
+
* flow delivers the endpoint, which is injected via
|
|
12
|
+
* {@link LocalDwnDiscovery.setCachedEndpoint | setCachedEndpoint()}.
|
|
12
13
|
*
|
|
13
|
-
* @see https://github.com/enboxorg/enbox/issues/
|
|
14
|
+
* @see https://github.com/enboxorg/enbox/issues/677
|
|
14
15
|
* @module
|
|
15
16
|
*/
|
|
16
17
|
import type { EnboxRpc } from '@enbox/dwn-clients';
|
|
17
18
|
import type { DwnDiscoveryFile } from './dwn-discovery-file.js';
|
|
18
|
-
/**
|
|
19
|
-
* Well-known ports the local DWN desktop app may bind to.
|
|
20
|
-
*
|
|
21
|
-
* Per the DWN Transport Spec, clients probe ports `55500` through `55509`
|
|
22
|
-
* (inclusive). Port `3000` is included as a development convenience.
|
|
23
|
-
*
|
|
24
|
-
* @see https://identity.foundation/dwn-transport/#port-probing
|
|
25
|
-
*/
|
|
26
|
-
export declare const localDwnPortCandidates: readonly [3000, 55500, 55501, 55502, 55503, 55504, 55505, 55506, 55507, 55508, 55509];
|
|
27
|
-
/**
|
|
28
|
-
* Hosts probed when discovering a local DWN server.
|
|
29
|
-
*
|
|
30
|
-
* Per the DWN Transport Spec, clients MUST use `127.0.0.1` rather than
|
|
31
|
-
* `localhost` to avoid DNS resolution ambiguity.
|
|
32
|
-
*
|
|
33
|
-
* @see https://identity.foundation/dwn-transport/#port-probing
|
|
34
|
-
*/
|
|
35
|
-
export declare const localDwnHostCandidates: readonly ["127.0.0.1"];
|
|
36
19
|
/**
|
|
37
20
|
* Controls how the agent discovers and routes to a local DWN server.
|
|
38
21
|
*
|
|
39
22
|
* - `'off'` — (default) skip local discovery entirely.
|
|
40
|
-
* - `'prefer'` —
|
|
23
|
+
* - `'prefer'` — try local DWN first; fall back to DID-document endpoints.
|
|
41
24
|
* - `'only'` — require a local server; throw if none is found.
|
|
42
25
|
*/
|
|
43
26
|
export type LocalDwnStrategy = 'prefer' | 'only' | 'off';
|
|
@@ -51,7 +34,7 @@ export declare function normalizeBaseUrl(url: string): string;
|
|
|
51
34
|
* Results are cached for {@link _cacheTtlMs} milliseconds (default 10 s) to
|
|
52
35
|
* avoid repeated I/O on hot paths such as sync.
|
|
53
36
|
*
|
|
54
|
-
* @example Discovery with file-based channel
|
|
37
|
+
* @example Discovery with file-based channel (CLI / native)
|
|
55
38
|
* ```ts
|
|
56
39
|
* import { DwnDiscoveryFile } from './dwn-discovery-file.js';
|
|
57
40
|
*
|
|
@@ -60,7 +43,7 @@ export declare function normalizeBaseUrl(url: string): string;
|
|
|
60
43
|
* const endpoint = await discovery.getEndpoint();
|
|
61
44
|
* ```
|
|
62
45
|
*
|
|
63
|
-
* @example Browser: inject cached endpoint from `dwn://
|
|
46
|
+
* @example Browser: inject cached endpoint from `dwn://connect` redirect
|
|
64
47
|
* ```ts
|
|
65
48
|
* const discovery = new LocalDwnDiscovery(rpcClient);
|
|
66
49
|
* discovery.setCachedEndpoint('http://127.0.0.1:55557');
|
|
@@ -82,11 +65,18 @@ export declare class LocalDwnDiscovery {
|
|
|
82
65
|
* 2. `~/.enbox/dwn.json` discovery file (if a {@link DwnDiscoveryFile}
|
|
83
66
|
* was provided). The endpoint from the file is validated via
|
|
84
67
|
* `GET /info` to ensure the server is still running.
|
|
85
|
-
*
|
|
68
|
+
*
|
|
69
|
+
* If neither channel finds an endpoint, the result (`undefined`) is
|
|
70
|
+
* cached to avoid repeated discovery file reads on hot paths.
|
|
71
|
+
*
|
|
72
|
+
* In browser environments (where no discovery file is available), the
|
|
73
|
+
* endpoint must be injected externally via
|
|
74
|
+
* {@link setCachedEndpoint | setCachedEndpoint()} — typically after a
|
|
75
|
+
* `dwn://connect` redirect delivers the endpoint in the URL fragment.
|
|
86
76
|
*/
|
|
87
77
|
getEndpoint(): Promise<string | undefined>;
|
|
88
78
|
/**
|
|
89
|
-
* Inject a cached endpoint (e.g. from a `dwn://
|
|
79
|
+
* Inject a cached endpoint (e.g. from a `dwn://connect` browser redirect
|
|
90
80
|
* or from `localStorage`). The endpoint is validated via `GET /info` before
|
|
91
81
|
* caching.
|
|
92
82
|
*
|
|
@@ -104,12 +94,6 @@ export declare class LocalDwnDiscovery {
|
|
|
104
94
|
* validation. Returns `undefined` otherwise.
|
|
105
95
|
*/
|
|
106
96
|
private _tryDiscoveryFile;
|
|
107
|
-
/**
|
|
108
|
-
* Sequential HTTP probe on well-known localhost port candidates.
|
|
109
|
-
* Returns the first endpoint whose `GET /info` response identifies
|
|
110
|
-
* as `@enbox/dwn-server`, or `undefined` if none is found.
|
|
111
|
-
*/
|
|
112
|
-
private _probePortCandidates;
|
|
113
97
|
/**
|
|
114
98
|
* Call `GET /info` on the endpoint and check that
|
|
115
99
|
* `serverInfo.server === '@enbox/dwn-server'`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-dwn.d.ts","sourceRoot":"","sources":["../../src/local-dwn.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"local-dwn.d.ts","sourceRoot":"","sources":["../../src/local-dwn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AAEzD,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,sBAAsB,CAAC;AAEtD,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,iBAAiB;IAK1B,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,cAAc,CAAC;IANzB,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,YAAY,CAAK;gBAGf,UAAU,EAAE,QAAQ,EACpB,WAAW,SAAS,EACpB,cAAc,CAAC,EAAE,gBAAgB,YAAA;IAG3C;;;;;;;;;;;;;;;;;OAiBG;IACU,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAmBvD;;;;;;OAMG;IACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASlE;;;OAGG;IACI,UAAU,IAAI,IAAI;IAOzB;;;;OAIG;YACW,iBAAiB;IAmB/B;;;OAGG;YACW,iBAAiB;IAS/B,wCAAwC;IACxC,OAAO,CAAC,cAAc;CAIvB"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enbox/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/esm/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"clean": "
|
|
10
|
-
"build:esm": "
|
|
11
|
-
"build:browser": "
|
|
9
|
+
"clean": "rm -rf dist",
|
|
10
|
+
"build:esm": "rm -rf dist/esm dist/types && bun tsc -p tsconfig.json",
|
|
11
|
+
"build:browser": "rm -rf dist/browser.mjs && bun ../../build/browser-bundle.js --node-shims",
|
|
12
12
|
"build": "bun run clean && bun run build:esm && bun run build:browser",
|
|
13
13
|
"lint": "eslint . --max-warnings 0",
|
|
14
14
|
"lint:fix": "eslint . --fix",
|
|
@@ -78,25 +78,23 @@
|
|
|
78
78
|
"@enbox/dids": "0.0.9",
|
|
79
79
|
"abstract-level": "1.0.4",
|
|
80
80
|
"ed25519-keygen": "0.4.11",
|
|
81
|
-
"level": "8.0.
|
|
81
|
+
"level": "8.0.1",
|
|
82
82
|
"ms": "2.1.3",
|
|
83
83
|
"ulidx": "2.1.0"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@types/dns-packet": "5.6.4",
|
|
87
|
-
"@types/ms": "0.7.
|
|
88
|
-
"@types/node": "
|
|
87
|
+
"@types/ms": "0.7.34",
|
|
88
|
+
"@types/node": "22.19.15",
|
|
89
89
|
"@types/sinon": "17.0.3",
|
|
90
90
|
"@typescript-eslint/eslint-plugin": "8.32.1",
|
|
91
91
|
"@typescript-eslint/parser": "8.32.1",
|
|
92
92
|
"@vitest/browser-playwright": "4.0.18",
|
|
93
93
|
"@vitest/coverage-istanbul": "4.0.18",
|
|
94
|
-
"
|
|
95
|
-
"bun-types": "latest",
|
|
94
|
+
"bun-types": "1.3.10",
|
|
96
95
|
"eslint": "9.7.0",
|
|
97
|
-
"rimraf": "4.4.0",
|
|
98
96
|
"sinon": "18.0.0",
|
|
99
|
-
"typescript": "5.
|
|
97
|
+
"typescript": "5.9.3",
|
|
100
98
|
"vitest": "4.0.18"
|
|
101
99
|
}
|
|
102
100
|
}
|
package/src/connect.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
import type {
|
|
3
|
-
import type { DwnPermissionScope, DwnProtocolDefinition
|
|
2
|
+
import type { ConnectPushedResponse, EnboxConnectResponse } from './enbox-connect-protocol.js';
|
|
3
|
+
import type { DwnPermissionScope, DwnProtocolDefinition } from './index.js';
|
|
4
4
|
|
|
5
5
|
import { CryptoUtils } from '@enbox/crypto';
|
|
6
6
|
import { DidJwk } from '@enbox/dids';
|
|
7
|
-
import {
|
|
7
|
+
import { EnboxConnectProtocol } from './enbox-connect-protocol.js';
|
|
8
8
|
import { pollWithTtl } from './utils.js';
|
|
9
9
|
import { Convert, logger } from '@enbox/common';
|
|
10
10
|
import { DwnInterfaceName, DwnMethodName } from '@enbox/dwn-sdk-js';
|
|
@@ -21,8 +21,8 @@ async function initClient({
|
|
|
21
21
|
onWalletUriReady,
|
|
22
22
|
validatePin,
|
|
23
23
|
}: WalletConnectOptions): Promise<{
|
|
24
|
-
delegateGrants:
|
|
25
|
-
delegatePortableDid:
|
|
24
|
+
delegateGrants: EnboxConnectResponse['delegateGrants'];
|
|
25
|
+
delegatePortableDid: EnboxConnectResponse['delegatePortableDid'];
|
|
26
26
|
connectedDid: string;
|
|
27
27
|
} | undefined> {
|
|
28
28
|
// ephemeral client did for ECDH, signing, verification
|
|
@@ -35,40 +35,36 @@ async function initClient({
|
|
|
35
35
|
// await Oidc.generateCodeChallenge();
|
|
36
36
|
const encryptionKey = CryptoUtils.randomBytes(32);
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
const callbackEndpoint =
|
|
38
|
+
// Build callback URL for the connect request.
|
|
39
|
+
const callbackEndpoint = EnboxConnectProtocol.buildConnectUrl({
|
|
40
40
|
baseURL : connectServerUrl,
|
|
41
41
|
endpoint : 'callback',
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
const request = await
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
redirect_uri : callbackEndpoint,
|
|
49
|
-
// custom properties:
|
|
50
|
-
// code_challenge : codeChallengeBase64Url,
|
|
51
|
-
// code_challenge_method : 'S256',
|
|
44
|
+
// Build the connect request.
|
|
45
|
+
const request = await EnboxConnectProtocol.createConnectRequest({
|
|
46
|
+
clientDid : clientDid.uri,
|
|
47
|
+
callbackUrl : callbackEndpoint,
|
|
52
48
|
permissionRequests : permissionRequests,
|
|
53
|
-
displayName,
|
|
49
|
+
appName : displayName,
|
|
54
50
|
});
|
|
55
51
|
|
|
56
|
-
// Sign the
|
|
57
|
-
const requestJwt = await
|
|
52
|
+
// Sign the request as a JWT.
|
|
53
|
+
const requestJwt = await EnboxConnectProtocol.signJwt({
|
|
58
54
|
did : clientDid,
|
|
59
|
-
data : request,
|
|
55
|
+
data : request as unknown as Record<string, unknown>,
|
|
60
56
|
});
|
|
61
57
|
|
|
62
58
|
if (!requestJwt) {
|
|
63
59
|
throw new Error('Unable to sign requestObject');
|
|
64
60
|
}
|
|
65
|
-
// Encrypt the
|
|
66
|
-
const requestObjectJwe = await
|
|
61
|
+
// Encrypt the request JWT with the symmetric key.
|
|
62
|
+
const requestObjectJwe = await EnboxConnectProtocol.encryptRequest({
|
|
67
63
|
jwt: requestJwt,
|
|
68
64
|
encryptionKey,
|
|
69
65
|
});
|
|
70
66
|
|
|
71
|
-
const pushedAuthorizationRequestEndpoint =
|
|
67
|
+
const pushedAuthorizationRequestEndpoint = EnboxConnectProtocol.buildConnectUrl({
|
|
72
68
|
baseURL : connectServerUrl,
|
|
73
69
|
endpoint : 'pushedAuthorizationRequest',
|
|
74
70
|
});
|
|
@@ -86,7 +82,7 @@ async function initClient({
|
|
|
86
82
|
throw new Error(`${parResponse.status}: ${parResponse.statusText}`);
|
|
87
83
|
}
|
|
88
84
|
|
|
89
|
-
const parData:
|
|
85
|
+
const parData: ConnectPushedResponse = await parResponse.json();
|
|
90
86
|
|
|
91
87
|
// a deeplink to a compatible wallet. if the wallet scans this link it should receive
|
|
92
88
|
// a route to its Connect provider flow and the params of where to fetch the auth request.
|
|
@@ -101,7 +97,7 @@ async function initClient({
|
|
|
101
97
|
// call user's callback so they can send the URI to the wallet as they see fit
|
|
102
98
|
onWalletUriReady(generatedWalletUri.toString());
|
|
103
99
|
|
|
104
|
-
const tokenUrl =
|
|
100
|
+
const tokenUrl = EnboxConnectProtocol.buildConnectUrl({
|
|
105
101
|
baseURL : connectServerUrl,
|
|
106
102
|
endpoint : 'token',
|
|
107
103
|
tokenParam : request.state,
|
|
@@ -113,36 +109,35 @@ async function initClient({
|
|
|
113
109
|
if (authResponse) {
|
|
114
110
|
const jwe = await authResponse?.text();
|
|
115
111
|
|
|
116
|
-
//
|
|
112
|
+
// Get the PIN from the user and use it as AAD to decrypt.
|
|
117
113
|
const pin = await validatePin();
|
|
118
|
-
const jwt = await
|
|
119
|
-
const
|
|
114
|
+
const jwt = await EnboxConnectProtocol.decryptResponse(clientDid, jwe, pin);
|
|
115
|
+
const verifiedResponse = (await EnboxConnectProtocol.verifyJwt({
|
|
120
116
|
jwt,
|
|
121
|
-
})) as
|
|
117
|
+
})) as unknown as EnboxConnectResponse;
|
|
122
118
|
|
|
123
119
|
return {
|
|
124
|
-
delegateGrants :
|
|
125
|
-
delegatePortableDid :
|
|
126
|
-
connectedDid :
|
|
120
|
+
delegateGrants : verifiedResponse.delegateGrants,
|
|
121
|
+
delegatePortableDid : verifiedResponse.delegatePortableDid,
|
|
122
|
+
connectedDid : verifiedResponse.providerDid,
|
|
127
123
|
};
|
|
128
124
|
}
|
|
129
125
|
}
|
|
130
126
|
|
|
131
127
|
/**
|
|
132
|
-
*
|
|
133
|
-
* a did from a provider.
|
|
128
|
+
* Options for initiating a wallet connect flow (remote, relay-mediated).
|
|
134
129
|
*/
|
|
135
130
|
export type WalletConnectOptions = {
|
|
136
|
-
/** The user
|
|
131
|
+
/** The user-friendly name of the app, displayed in the wallet consent UI. */
|
|
137
132
|
displayName: string;
|
|
138
133
|
|
|
139
|
-
/** The URL of the
|
|
134
|
+
/** The URL of the connect server which relays messages between the app and wallet. */
|
|
140
135
|
connectServerUrl: string;
|
|
141
136
|
|
|
142
137
|
/**
|
|
143
|
-
* The URI of the
|
|
144
|
-
*
|
|
145
|
-
* @example `
|
|
138
|
+
* The URI of the wallet app. Query params (`request_uri`, `encryption_key`)
|
|
139
|
+
* are appended and passed to `onWalletUriReady`.
|
|
140
|
+
* @example `enbox://connect` or `http://localhost:3000/`
|
|
146
141
|
*/
|
|
147
142
|
walletUri: string;
|
|
148
143
|
|
|
@@ -154,20 +149,16 @@ export type WalletConnectOptions = {
|
|
|
154
149
|
permissionRequests: ConnectPermissionRequest[];
|
|
155
150
|
|
|
156
151
|
/**
|
|
157
|
-
*
|
|
158
|
-
* The
|
|
159
|
-
* The query params are `{ request_uri: string; encryption_key: string; }`
|
|
160
|
-
* The wallet will use the `request_uri to contact the intermediary server's `authorize` endpoint
|
|
161
|
-
* and pull down the {@link EnboxConnectAuthRequest} and use the `encryption_key` to decrypt it.
|
|
152
|
+
* Called with the wallet URI including query params (`request_uri`, `encryption_key`).
|
|
153
|
+
* The app should render this as a QR code or use it as a deep link.
|
|
162
154
|
*
|
|
163
|
-
* @param uri - The URI
|
|
155
|
+
* @param uri - The wallet URI with connect payload.
|
|
164
156
|
*/
|
|
165
157
|
onWalletUriReady: (uri: string) => void;
|
|
166
158
|
|
|
167
159
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* token endpoint by the client inside of Connect.
|
|
160
|
+
* Called to collect the PIN from the user. The PIN is used as AAD
|
|
161
|
+
* when decrypting the connect response from the relay.
|
|
171
162
|
*
|
|
172
163
|
* @returns A promise that resolves to the PIN as a string.
|
|
173
164
|
*/
|
package/src/dwn-api.ts
CHANGED
|
@@ -86,8 +86,9 @@ import {
|
|
|
86
86
|
writeContextKeyRecord as writeContextKeyRecordFn,
|
|
87
87
|
} from './dwn-key-delivery.js';
|
|
88
88
|
|
|
89
|
-
//
|
|
90
|
-
|
|
89
|
+
// NOTE: upgradeExternalRootRecord is disabled — see TODO in postWriteKeyDelivery().
|
|
90
|
+
// The module is kept for reference but no longer imported.
|
|
91
|
+
// import { upgradeExternalRootRecord as upgradeExternalRootRecordFn } from './dwn-record-upgrade.js';
|
|
91
92
|
|
|
92
93
|
// Import extracted protocol definition fetching functions
|
|
93
94
|
import {
|
|
@@ -103,9 +104,11 @@ type DwnMessageWithBlob<T extends DwnInterface> = {
|
|
|
103
104
|
|
|
104
105
|
type DwnApiParams = {
|
|
105
106
|
agent?: EnboxPlatformAgent;
|
|
106
|
-
dwn: Dwn;
|
|
107
107
|
localDwnStrategy?: LocalDwnStrategy;
|
|
108
|
-
}
|
|
108
|
+
} & (
|
|
109
|
+
| { dwn: Dwn; localDwnEndpoint?: never }
|
|
110
|
+
| { dwn?: never; localDwnEndpoint: string }
|
|
111
|
+
);
|
|
109
112
|
|
|
110
113
|
interface DwnApiCreateDwnParams extends Partial<DwnConfig> {
|
|
111
114
|
dataPath?: string;
|
|
@@ -122,8 +125,17 @@ export class AgentDwnApi {
|
|
|
122
125
|
|
|
123
126
|
/**
|
|
124
127
|
* The DWN instance to use for this API.
|
|
128
|
+
* `undefined` in remote mode — all operations route through RPC to
|
|
129
|
+
* the local DWN server endpoint.
|
|
130
|
+
*/
|
|
131
|
+
private _dwn?: Dwn;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The local DWN server endpoint for remote mode.
|
|
135
|
+
* When set, `_dwn` is `undefined` and `processRequest()` routes
|
|
136
|
+
* through `sendDwnRpcRequest()`.
|
|
125
137
|
*/
|
|
126
|
-
private
|
|
138
|
+
private _localDwnEndpoint?: string;
|
|
127
139
|
|
|
128
140
|
/**
|
|
129
141
|
* Protocol definition cache — TTL 30 minutes. Protocols rarely change.
|
|
@@ -166,12 +178,17 @@ export class AgentDwnApi {
|
|
|
166
178
|
/** Lazy-initialized local DWN discovery instance. */
|
|
167
179
|
private _localDwnDiscovery?: LocalDwnDiscovery;
|
|
168
180
|
|
|
169
|
-
constructor(
|
|
181
|
+
constructor(params: DwnApiParams) {
|
|
182
|
+
const { agent, localDwnStrategy = 'prefer' } = params;
|
|
183
|
+
|
|
170
184
|
// If an agent is provided, set it as the execution context for this API.
|
|
171
185
|
this._agent = agent;
|
|
172
186
|
|
|
173
|
-
// Set the DWN instance
|
|
174
|
-
this._dwn = dwn;
|
|
187
|
+
// Set the DWN instance (undefined in remote mode).
|
|
188
|
+
this._dwn = 'dwn' in params ? params.dwn : undefined;
|
|
189
|
+
|
|
190
|
+
// Set the remote endpoint (undefined in local mode).
|
|
191
|
+
this._localDwnEndpoint = 'localDwnEndpoint' in params ? params.localDwnEndpoint : undefined;
|
|
175
192
|
|
|
176
193
|
// Set the local DWN discovery strategy.
|
|
177
194
|
this._localDwnStrategy = localDwnStrategy;
|
|
@@ -186,6 +203,14 @@ export class AgentDwnApi {
|
|
|
186
203
|
}
|
|
187
204
|
}
|
|
188
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Whether the API is operating in remote mode (no in-process DWN).
|
|
208
|
+
* In remote mode, all DWN operations are routed through RPC.
|
|
209
|
+
*/
|
|
210
|
+
get isRemoteMode(): boolean {
|
|
211
|
+
return this._dwn === undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
189
214
|
/**
|
|
190
215
|
* Retrieves the `EnboxPlatformAgent` execution context.
|
|
191
216
|
*
|
|
@@ -220,7 +245,7 @@ export class AgentDwnApi {
|
|
|
220
245
|
}
|
|
221
246
|
|
|
222
247
|
/**
|
|
223
|
-
* Inject a cached local DWN endpoint (e.g. from a `dwn://
|
|
248
|
+
* Inject a cached local DWN endpoint (e.g. from a `dwn://connect`
|
|
224
249
|
* browser redirect or from persisted storage). The endpoint is validated
|
|
225
250
|
* via `GET /info` before being accepted.
|
|
226
251
|
*
|
|
@@ -257,8 +282,8 @@ export class AgentDwnApi {
|
|
|
257
282
|
if (this._localDwnStrategy === 'only') {
|
|
258
283
|
if (!localDwnEndpoint) {
|
|
259
284
|
throw new Error(
|
|
260
|
-
`AgentDwnApi: Local DWN strategy is 'only' but no local
|
|
261
|
-
`
|
|
285
|
+
`AgentDwnApi: Local DWN strategy is 'only' but no local DWN endpoint was discovered. ` +
|
|
286
|
+
`Ensure the local DWN server is running and discoverable via the discovery file (~/.enbox/dwn.json) or dwn://connect.`
|
|
262
287
|
);
|
|
263
288
|
}
|
|
264
289
|
|
|
@@ -288,6 +313,11 @@ export class AgentDwnApi {
|
|
|
288
313
|
|
|
289
314
|
/** Lazily retrieves the local DWN server endpoint via discovery. */
|
|
290
315
|
private async getLocalDwnEndpoint(): Promise<string | undefined> {
|
|
316
|
+
// In remote mode, the endpoint is always known.
|
|
317
|
+
if (this._localDwnEndpoint) {
|
|
318
|
+
return this._localDwnEndpoint;
|
|
319
|
+
}
|
|
320
|
+
|
|
291
321
|
this._localDwnDiscovery ??= new LocalDwnDiscovery(
|
|
292
322
|
this.agent.rpc,
|
|
293
323
|
10_000,
|
|
@@ -364,6 +394,14 @@ export class AgentDwnApi {
|
|
|
364
394
|
* the DWN instance and not `agent.dwn.dwn`.
|
|
365
395
|
*/
|
|
366
396
|
get node(): Dwn {
|
|
397
|
+
if (!this._dwn) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
'AgentDwnApi: The in-process DWN instance is not available. ' +
|
|
400
|
+
'The agent is operating in remote mode (local DWN server at ' +
|
|
401
|
+
`'${this._localDwnEndpoint}'). Use processRequest() instead ` +
|
|
402
|
+
'of accessing the DWN node directly.'
|
|
403
|
+
);
|
|
404
|
+
}
|
|
367
405
|
return this._dwn;
|
|
368
406
|
}
|
|
369
407
|
|
|
@@ -407,10 +445,35 @@ export class AgentDwnApi {
|
|
|
407
445
|
// processing, passing along the target DID, the message, and any associated data stream.
|
|
408
446
|
// - If `store` is set to false, it immediately returns a simulated 'accepted' status without
|
|
409
447
|
// storing the message/data in the DWN node.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
448
|
+
let reply: DwnMessageReply[T];
|
|
449
|
+
|
|
450
|
+
if (request.store === false) {
|
|
451
|
+
reply = { status: { code: 202, detail: 'Accepted' } };
|
|
452
|
+
} else if (this._dwn) {
|
|
453
|
+
// Local mode: process directly with the in-process DWN.
|
|
454
|
+
reply = await this._dwn.processMessage(
|
|
455
|
+
request.target, message,
|
|
456
|
+
{ dataStream: dataStream as any, subscriptionHandler },
|
|
457
|
+
);
|
|
458
|
+
} else {
|
|
459
|
+
// Remote mode: route through RPC to the local DWN server.
|
|
460
|
+
// TODO(#713): This buffers the entire stream into memory before
|
|
461
|
+
// re-streaming it over HTTP. The RPC transport should accept a
|
|
462
|
+
// ReadableStream directly to avoid the extra copy.
|
|
463
|
+
let data: Blob | undefined;
|
|
464
|
+
if (dataStream) {
|
|
465
|
+
const bytes = await DataStream.toBytes(dataStream);
|
|
466
|
+
data = new Blob([bytes as BlobPart]);
|
|
467
|
+
}
|
|
413
468
|
|
|
469
|
+
reply = await this.sendDwnRpcRequest({
|
|
470
|
+
targetDid : request.target,
|
|
471
|
+
dwnEndpointUrls : [this._localDwnEndpoint!],
|
|
472
|
+
message,
|
|
473
|
+
data,
|
|
474
|
+
subscriptionHandler,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
414
477
|
|
|
415
478
|
// Post-write key delivery: detect new participants and write contextKey records.
|
|
416
479
|
await this.postWriteKeyDelivery(request, message, reply);
|
|
@@ -427,6 +490,46 @@ export class AgentDwnApi {
|
|
|
427
490
|
};
|
|
428
491
|
}
|
|
429
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Process a pre-constructed DWN message against the local DWN (in-process
|
|
495
|
+
* or remote server). Used by the sync engine to store messages that were
|
|
496
|
+
* already fetched from a remote DWN.
|
|
497
|
+
*
|
|
498
|
+
* Unlike {@link processRequest}, this method does NOT construct a new
|
|
499
|
+
* message — it takes an already-signed `GenericMessage` and routes it
|
|
500
|
+
* to the appropriate backend.
|
|
501
|
+
*
|
|
502
|
+
* @param tenant - The DID of the DWN tenant (target).
|
|
503
|
+
* @param message - The pre-constructed DWN message.
|
|
504
|
+
* @param options - Optional data stream and subscription handler.
|
|
505
|
+
* @returns The reply from processing the message.
|
|
506
|
+
*/
|
|
507
|
+
public async processRawMessage(
|
|
508
|
+
tenant: string,
|
|
509
|
+
message: GenericMessage,
|
|
510
|
+
options?: { dataStream?: ReadableStream<Uint8Array> },
|
|
511
|
+
): Promise<{ status: { code: number; detail: string } }> {
|
|
512
|
+
if (this._dwn) {
|
|
513
|
+
return this._dwn.processMessage(tenant, message, { dataStream: options?.dataStream });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// TODO(#713): This buffers the entire stream into memory before
|
|
517
|
+
// re-streaming it over HTTP. The RPC transport should accept a
|
|
518
|
+
// ReadableStream directly to avoid the extra copy.
|
|
519
|
+
let data: Blob | undefined;
|
|
520
|
+
if (options?.dataStream) {
|
|
521
|
+
const bytes = await DataStream.toBytes(options.dataStream);
|
|
522
|
+
data = new Blob([bytes as BlobPart]);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return this.sendDwnRpcRequest({
|
|
526
|
+
targetDid : tenant,
|
|
527
|
+
dwnEndpointUrls : [this._localDwnEndpoint!],
|
|
528
|
+
message : message as DwnMessage[DwnInterface],
|
|
529
|
+
data,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
430
533
|
public async sendRequest<T extends DwnInterface>(
|
|
431
534
|
request: SendDwnRequest<T>
|
|
432
535
|
): Promise<DwnResponse<T>> {
|
|
@@ -547,17 +650,31 @@ export class AgentDwnApi {
|
|
|
547
650
|
const isMultiParty = isMultiPartyContextFn(protocolDefinition, rootPathSegment);
|
|
548
651
|
|
|
549
652
|
if (isExternallyAuthored && isRootRecord && isMultiParty) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
653
|
+
// TODO: Reactive root-record upgrade is disabled and needs redesign.
|
|
654
|
+
//
|
|
655
|
+
// The previous implementation (`upgradeExternalRootRecord` in
|
|
656
|
+
// `dwn-record-upgrade.ts`) bypassed DWN SDK conflict resolution by
|
|
657
|
+
// directly manipulating messageStore/stateIndex/eventLog internals.
|
|
658
|
+
// This is incompatible with remote DWN operation (local DWN server
|
|
659
|
+
// accessed via RPC) where the agent has no direct access to the
|
|
660
|
+
// server's storage layer.
|
|
661
|
+
//
|
|
662
|
+
// The correct approach is either:
|
|
663
|
+
// (a) Perform the upgrade BEFORE the initial processMessage() call
|
|
664
|
+
// (pre-store augmentation), so the message is stored in its
|
|
665
|
+
// final form on the first pass — no replacement needed.
|
|
666
|
+
// (b) Add DWN SDK support for same-timestamp owner-augmented
|
|
667
|
+
// replacements in the RecordsWrite handler.
|
|
668
|
+
//
|
|
669
|
+
// Bumping messageTimestamp is NOT viable because the author's
|
|
670
|
+
// signature payload contains descriptorCid (which includes the
|
|
671
|
+
// timestamp). The owner does not have the external author's signing
|
|
672
|
+
// key and cannot re-sign.
|
|
673
|
+
//
|
|
674
|
+
// Until this is redesigned, externally-authored root records in
|
|
675
|
+
// multi-party encrypted contexts will only have ProtocolPath
|
|
676
|
+
// encryption. Context key holders will not be able to decrypt
|
|
677
|
+
// these records via ProtocolContext.
|
|
561
678
|
}
|
|
562
679
|
|
|
563
680
|
const newParticipants = detectNewParticipantsFn({
|
|
@@ -1160,6 +1277,17 @@ export class AgentDwnApi {
|
|
|
1160
1277
|
tenantDid: string,
|
|
1161
1278
|
protocolUri: string,
|
|
1162
1279
|
): Promise<ProtocolDefinition | undefined> {
|
|
1280
|
+
if (!this._dwn) {
|
|
1281
|
+
// Remote mode: query via RPC (same as fetchRemoteProtocolDefinition,
|
|
1282
|
+
// but for locally-managed DIDs). The remote protocol definition
|
|
1283
|
+
// cache uses a different key prefix, so we use a dedicated call.
|
|
1284
|
+
try {
|
|
1285
|
+
return await this.fetchRemoteProtocolDefinition(tenantDid, protocolUri);
|
|
1286
|
+
} catch {
|
|
1287
|
+
// Protocol not found — return undefined (consistent with local mode).
|
|
1288
|
+
return undefined;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1163
1291
|
return getProtocolDefinitionFn(
|
|
1164
1292
|
tenantDid, protocolUri, this._dwn,
|
|
1165
1293
|
this.getSigner.bind(this), this._protocolDefinitionCache,
|
|
@@ -1233,7 +1361,19 @@ export class AgentDwnApi {
|
|
|
1233
1361
|
signer
|
|
1234
1362
|
});
|
|
1235
1363
|
|
|
1236
|
-
|
|
1364
|
+
let result: any;
|
|
1365
|
+
|
|
1366
|
+
if (this._dwn) {
|
|
1367
|
+
// Local mode: process directly with the in-process DWN.
|
|
1368
|
+
result = await this._dwn.processMessage(author, messagesRead.message);
|
|
1369
|
+
} else {
|
|
1370
|
+
// Remote mode: route through RPC to the local DWN server.
|
|
1371
|
+
result = await this.sendDwnRpcRequest({
|
|
1372
|
+
targetDid : author,
|
|
1373
|
+
dwnEndpointUrls : [this._localDwnEndpoint!],
|
|
1374
|
+
message : messagesRead.message,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1237
1377
|
|
|
1238
1378
|
if (result.status.code !== 200) {
|
|
1239
1379
|
throw new Error(`AgentDwnApi: Failed to read message, response status: ${result.status.code} - ${result.status.detail}`);
|
|
@@ -1245,9 +1385,15 @@ export class AgentDwnApi {
|
|
|
1245
1385
|
const dwnMessageWithBlob: DwnMessageWithBlob<T> = { message };
|
|
1246
1386
|
// If the message is a RecordsWrite, data will be present in the form of a stream
|
|
1247
1387
|
|
|
1248
|
-
if (isRecordsWrite(messageEntry)
|
|
1249
|
-
|
|
1250
|
-
|
|
1388
|
+
if (isRecordsWrite(messageEntry)) {
|
|
1389
|
+
// The processMessage result includes a `data` ReadableStream for
|
|
1390
|
+
// RecordsWrite entries, but the RecordsWrite type doesn't declare
|
|
1391
|
+
// it. Access via index signature to avoid the type mismatch.
|
|
1392
|
+
const entryData = (messageEntry as unknown as Record<string, unknown>)['data'] as ReadableStream<Uint8Array> | undefined;
|
|
1393
|
+
if (entryData) {
|
|
1394
|
+
const dataBytes = await DataStream.toBytes(entryData);
|
|
1395
|
+
dwnMessageWithBlob.data = new Blob([ dataBytes as BlobPart ], { type: messageEntry.message.descriptor.dataFormat });
|
|
1396
|
+
}
|
|
1251
1397
|
}
|
|
1252
1398
|
|
|
1253
1399
|
return dwnMessageWithBlob;
|
|
@@ -139,7 +139,7 @@ export const DISCOVERY_FILENAME = 'dwn.json';
|
|
|
139
139
|
* Reads, writes, and validates the `~/.enbox/dwn.json` discovery file.
|
|
140
140
|
*
|
|
141
141
|
* This is the **file-based discovery channel** for CLI and native apps.
|
|
142
|
-
* It is complementary to the `dwn://
|
|
142
|
+
* It is complementary to the `dwn://connect` browser redirect flow.
|
|
143
143
|
*
|
|
144
144
|
* @example Reading the discovery file
|
|
145
145
|
* ```ts
|