@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.
Files changed (57) hide show
  1. package/dist/browser.mjs +12 -30
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/connect.js +20 -24
  4. package/dist/esm/connect.js.map +1 -1
  5. package/dist/esm/dwn-api.js +149 -22
  6. package/dist/esm/dwn-api.js.map +1 -1
  7. package/dist/esm/dwn-discovery-file.js +1 -1
  8. package/dist/esm/dwn-discovery-payload.js +20 -21
  9. package/dist/esm/dwn-discovery-payload.js.map +1 -1
  10. package/dist/esm/dwn-key-delivery.js.map +1 -1
  11. package/dist/esm/{oidc.js → enbox-connect-protocol.js} +236 -248
  12. package/dist/esm/enbox-connect-protocol.js.map +1 -0
  13. package/dist/esm/enbox-user-agent.js +18 -5
  14. package/dist/esm/enbox-user-agent.js.map +1 -1
  15. package/dist/esm/index.js +4 -2
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/local-dwn.js +21 -51
  18. package/dist/esm/local-dwn.js.map +1 -1
  19. package/dist/esm/permissions-api.js.map +1 -1
  20. package/dist/esm/store-data.js.map +1 -1
  21. package/dist/esm/sync-engine-level.js +1 -1
  22. package/dist/esm/sync-engine-level.js.map +1 -1
  23. package/dist/esm/sync-messages.js +1 -1
  24. package/dist/esm/sync-messages.js.map +1 -1
  25. package/dist/types/connect.d.ts +15 -19
  26. package/dist/types/connect.d.ts.map +1 -1
  27. package/dist/types/dwn-api.d.ts +46 -6
  28. package/dist/types/dwn-api.d.ts.map +1 -1
  29. package/dist/types/dwn-discovery-file.d.ts +1 -1
  30. package/dist/types/dwn-discovery-payload.d.ts +18 -19
  31. package/dist/types/dwn-discovery-payload.d.ts.map +1 -1
  32. package/dist/types/enbox-connect-protocol.d.ts +220 -0
  33. package/dist/types/enbox-connect-protocol.d.ts.map +1 -0
  34. package/dist/types/enbox-user-agent.d.ts +10 -1
  35. package/dist/types/enbox-user-agent.d.ts.map +1 -1
  36. package/dist/types/index.d.ts +1 -2
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/dist/types/local-dwn.d.ts +16 -32
  39. package/dist/types/local-dwn.d.ts.map +1 -1
  40. package/package.json +9 -11
  41. package/src/connect.ts +38 -47
  42. package/src/dwn-api.ts +175 -29
  43. package/src/dwn-discovery-file.ts +1 -1
  44. package/src/dwn-discovery-payload.ts +23 -24
  45. package/src/dwn-key-delivery.ts +1 -1
  46. package/src/enbox-connect-protocol.ts +778 -0
  47. package/src/enbox-user-agent.ts +27 -4
  48. package/src/index.ts +4 -2
  49. package/src/local-dwn.ts +22 -53
  50. package/src/permissions-api.ts +3 -3
  51. package/src/store-data.ts +1 -1
  52. package/src/sync-engine-level.ts +1 -1
  53. package/src/sync-messages.ts +1 -1
  54. package/dist/esm/oidc.js.map +0 -1
  55. package/dist/types/oidc.d.ts +0 -250
  56. package/dist/types/oidc.d.ts.map +0 -1
  57. 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. **Port probing** (fallback) sequential HTTP `GET /info` on well-known
11
- * localhost ports. Works everywhere but is slower.
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/585
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'` — probe localhost first; fall back to DID-document endpoints.
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://register` redirect
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
- * 3. Sequential port probing on well-known localhost ports (fallback).
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://register` browser redirect
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;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,uFAAwF,CAAC;AAE5H;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,wBAAyB,CAAC;AAE7D;;;;;;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;;;;;;;;;;OAUG;IACU,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAoBvD;;;;;;OAMG;IACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASlE;;;OAGG;IACI,UAAU,IAAI,IAAI;IAOzB;;;;OAIG;YACW,iBAAiB;IAmB/B;;;;OAIG;YACW,oBAAoB;IAalC;;;OAGG;YACW,iBAAiB;IAS/B,wCAAwC;IACxC,OAAO,CAAC,cAAc;CAIvB"}
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.1",
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": "rimraf dist",
10
- "build:esm": "rimraf dist/esm dist/types && bun tsc -p tsconfig.json",
11
- "build:browser": "rimraf dist/browser.mjs && bun ../../build/browser-bundle.js --node-shims",
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.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.31",
88
- "@types/node": "20.14.8",
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
- "abstract-level": "1.0.4",
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.5.4",
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 { PushedAuthResponse } from './oidc.js';
3
- import type { DwnPermissionScope, DwnProtocolDefinition, EnboxConnectAuthResponse } from './index.js';
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 { Oidc } from './oidc.js';
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: EnboxConnectAuthResponse['delegateGrants'];
25
- delegatePortableDid: EnboxConnectAuthResponse['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
- // build callback URL to pass into the auth request
39
- const callbackEndpoint = Oidc.buildOidcUrl({
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
- // build the PAR request
45
- const request = await Oidc.createAuthRequest({
46
- client_id : clientDid.uri,
47
- scope : 'openid did:jwk',
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 Request Object using the Client DID's signing key.
57
- const requestJwt = await Oidc.signJwt({
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 Request Object JWT using the code challenge.
66
- const requestObjectJwe = await Oidc.encryptAuthRequest({
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 = Oidc.buildOidcUrl({
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: PushedAuthResponse = await parResponse.json();
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 = Oidc.buildOidcUrl({
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
- // get the pin from the user and use it as AAD to decrypt
112
+ // Get the PIN from the user and use it as AAD to decrypt.
117
113
  const pin = await validatePin();
118
- const jwt = await Oidc.decryptAuthResponse(clientDid, jwe, pin);
119
- const verifiedAuthResponse = (await Oidc.verifyJwt({
114
+ const jwt = await EnboxConnectProtocol.decryptResponse(clientDid, jwe, pin);
115
+ const verifiedResponse = (await EnboxConnectProtocol.verifyJwt({
120
116
  jwt,
121
- })) as EnboxConnectAuthResponse;
117
+ })) as unknown as EnboxConnectResponse;
122
118
 
123
119
  return {
124
- delegateGrants : verifiedAuthResponse.delegateGrants,
125
- delegatePortableDid : verifiedAuthResponse.delegatePortableDid,
126
- connectedDid : verifiedAuthResponse.iss,
120
+ delegateGrants : verifiedResponse.delegateGrants,
121
+ delegatePortableDid : verifiedResponse.delegatePortableDid,
122
+ connectedDid : verifiedResponse.providerDid,
127
123
  };
128
124
  }
129
125
  }
130
126
 
131
127
  /**
132
- * Initiates the wallet connect process. Used when a client wants to obtain
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 friendly name of the client/app to be displayed when prompting end-user with permission requests. */
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 intermediary server which relays messages between the client and provider. */
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 Provider (wallet).The `onWalletUriReady` will take this wallet
144
- * uri and add a payload to it which will be used to obtain and decrypt from the `request_uri`.
145
- * @example `web5://` or `http://localhost:3000/`.
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
- * The Connect API provides a URI to the wallet based on the `walletUri` plus a query params payload valid for 5 minutes.
158
- * The link can either be used as a deep link on the same device or a QR code for cross device or both.
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 returned by the Connect API to be passed to a provider.
155
+ * @param uri - The wallet URI with connect payload.
164
156
  */
165
157
  onWalletUriReady: (uri: string) => void;
166
158
 
167
159
  /**
168
- * Function that must be provided to submit the pin entered by the user on the client.
169
- * The pin is used to decrypt the {@link EnboxConnectAuthResponse} that was retrieved from the
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
- // Import extracted record upgrade function
90
- import { upgradeExternalRootRecord as upgradeExternalRootRecordFn } from './dwn-record-upgrade.js';
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 _dwn: Dwn;
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({ agent, dwn, localDwnStrategy = 'prefer' }: DwnApiParams) {
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 for this API.
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://register`
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 server is available ` +
261
- `on 127.0.0.1:{3000,55500-55509}`
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
- const reply: DwnMessageReply[T] = (request.store !== false)
411
- ? await this._dwn.processMessage(request.target, message, { dataStream: dataStream as any, subscriptionHandler })
412
- : { status: { code: 202, detail: 'Accepted' } };
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
- try {
551
- await upgradeExternalRootRecordFn(
552
- this.agent, request.target, recordsWriteMessage,
553
- this._dwn, this.getSigner.bind(this), this._contextKeyCache,
554
- );
555
- } catch (upgradeError: any) {
556
- console.warn(
557
- `AgentDwnApi: Reactive root-record upgrade failed for ` +
558
- `'${recordsWriteMessage.recordId}': ${upgradeError.message}`
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
- const result = await this._dwn.processMessage(author, messagesRead.message);
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) && messageEntry.data) {
1249
- const dataBytes = await DataStream.toBytes(messageEntry.data);
1250
- dwnMessageWithBlob.data = new Blob([ dataBytes ], { type: messageEntry.message.descriptor.dataFormat });
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://register` browser redirect flow.
142
+ * It is complementary to the `dwn://connect` browser redirect flow.
143
143
  *
144
144
  * @example Reading the discovery file
145
145
  * ```ts