@enbox/agent 0.3.1 → 0.5.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 (81) hide show
  1. package/dist/browser.mjs +12 -30
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/dwn-api.js +149 -22
  4. package/dist/esm/dwn-api.js.map +1 -1
  5. package/dist/esm/dwn-discovery-file.js +1 -1
  6. package/dist/esm/dwn-discovery-payload.js +20 -21
  7. package/dist/esm/dwn-discovery-payload.js.map +1 -1
  8. package/dist/esm/dwn-key-delivery.js.map +1 -1
  9. package/dist/esm/{oidc.js → enbox-connect-protocol.js} +219 -251
  10. package/dist/esm/enbox-connect-protocol.js.map +1 -0
  11. package/dist/esm/enbox-user-agent.js +19 -12
  12. package/dist/esm/enbox-user-agent.js.map +1 -1
  13. package/dist/esm/hd-identity-vault.js +11 -0
  14. package/dist/esm/hd-identity-vault.js.map +1 -1
  15. package/dist/esm/index.js +4 -4
  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/esm/test-harness.js +2 -3
  26. package/dist/esm/test-harness.js.map +1 -1
  27. package/dist/esm/types/dwn.js.map +1 -1
  28. package/dist/types/dwn-api.d.ts +46 -6
  29. package/dist/types/dwn-api.d.ts.map +1 -1
  30. package/dist/types/dwn-discovery-file.d.ts +1 -1
  31. package/dist/types/dwn-discovery-payload.d.ts +18 -19
  32. package/dist/types/dwn-discovery-payload.d.ts.map +1 -1
  33. package/dist/types/enbox-connect-protocol.d.ts +206 -0
  34. package/dist/types/enbox-connect-protocol.d.ts.map +1 -0
  35. package/dist/types/enbox-user-agent.d.ts +13 -8
  36. package/dist/types/enbox-user-agent.d.ts.map +1 -1
  37. package/dist/types/hd-identity-vault.d.ts +7 -0
  38. package/dist/types/hd-identity-vault.d.ts.map +1 -1
  39. package/dist/types/index.d.ts +1 -4
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/dist/types/local-dwn.d.ts +16 -32
  42. package/dist/types/local-dwn.d.ts.map +1 -1
  43. package/dist/types/test-harness.d.ts.map +1 -1
  44. package/dist/types/types/agent.d.ts +2 -7
  45. package/dist/types/types/agent.d.ts.map +1 -1
  46. package/dist/types/types/dwn.d.ts +0 -10
  47. package/dist/types/types/dwn.d.ts.map +1 -1
  48. package/dist/types/types/sync.d.ts +6 -0
  49. package/dist/types/types/sync.d.ts.map +1 -1
  50. package/package.json +14 -16
  51. package/src/dwn-api.ts +175 -29
  52. package/src/dwn-discovery-file.ts +1 -1
  53. package/src/dwn-discovery-payload.ts +23 -24
  54. package/src/dwn-key-delivery.ts +1 -1
  55. package/src/enbox-connect-protocol.ts +753 -0
  56. package/src/enbox-user-agent.ts +31 -18
  57. package/src/hd-identity-vault.ts +21 -0
  58. package/src/index.ts +4 -4
  59. package/src/local-dwn.ts +22 -53
  60. package/src/permissions-api.ts +3 -3
  61. package/src/store-data.ts +1 -1
  62. package/src/sync-engine-level.ts +1 -1
  63. package/src/sync-messages.ts +1 -1
  64. package/src/test-harness.ts +2 -3
  65. package/src/types/agent.ts +3 -14
  66. package/src/types/dwn.ts +1 -13
  67. package/src/types/sync.ts +7 -0
  68. package/dist/esm/connect.js +0 -180
  69. package/dist/esm/connect.js.map +0 -1
  70. package/dist/esm/oidc.js.map +0 -1
  71. package/dist/esm/sync-api.js +0 -64
  72. package/dist/esm/sync-api.js.map +0 -1
  73. package/dist/types/connect.d.ts +0 -88
  74. package/dist/types/connect.d.ts.map +0 -1
  75. package/dist/types/oidc.d.ts +0 -250
  76. package/dist/types/oidc.d.ts.map +0 -1
  77. package/dist/types/sync-api.d.ts +0 -40
  78. package/dist/types/sync-api.d.ts.map +0 -1
  79. package/src/connect.ts +0 -285
  80. package/src/oidc.ts +0 -864
  81. package/src/sync-api.ts +0 -75
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@enbox/agent",
3
- "version": "0.3.1",
3
+ "version": "0.5.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",
@@ -71,32 +71,30 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@scure/bip39": "1.2.2",
74
- "@enbox/dwn-clients": "0.1.0",
75
- "@enbox/dwn-sdk-js": "0.1.2",
76
- "@enbox/common": "0.0.7",
77
- "@enbox/crypto": "0.0.8",
78
- "@enbox/dids": "0.0.9",
74
+ "@enbox/dwn-clients": "0.2.0",
75
+ "@enbox/dwn-sdk-js": "0.2.0",
76
+ "@enbox/common": "0.1.0",
77
+ "@enbox/crypto": "0.1.0",
78
+ "@enbox/dids": "0.1.0",
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/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
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Shared types and utilities for the `dwn://register` discovery protocol.
2
+ * Shared types and utilities for the `dwn://connect` discovery protocol.
3
3
  *
4
4
  * The payload is the JSON data exchanged between the local DWN server
5
- * (electrobun-dwn) and the requesting app during the `dwn://register`
5
+ * (electrobun-dwn) and the requesting app during the `dwn://connect`
6
6
  * redirect flow. It is encoded as base64url and placed in the URL
7
7
  * fragment (`#`) of the callback URL.
8
8
  *
@@ -10,14 +10,13 @@
10
10
  * consumed from any environment (Bun, browser, Electrobun) without
11
11
  * triggering transitive dependency resolution issues.
12
12
  *
13
- * @see https://github.com/enboxorg/enbox/issues/586
14
13
  * @module
15
14
  */
16
15
 
17
16
  // ─── Types ────────────────────────────────────────────────────────
18
17
 
19
18
  /**
20
- * The JSON payload delivered via the URL fragment in a `dwn://register`
19
+ * The JSON payload delivered via the URL fragment in a `dwn://connect`
21
20
  * callback redirect.
22
21
  *
23
22
  * Intentionally minimal — everything beyond the endpoint (version,
@@ -29,9 +28,9 @@ export type DwnDiscoveryPayload = {
29
28
  };
30
29
 
31
30
  /**
32
- * Parsed result from a `dwn://register` URL.
31
+ * Parsed result from a `dwn://connect` URL.
33
32
  */
34
- export type DwnRegisterUrlParams = {
33
+ export type DwnConnectUrlParams = {
35
34
  /** The callback URL to redirect to with the discovery payload. */
36
35
  callback: string;
37
36
  };
@@ -41,17 +40,17 @@ export type DwnRegisterUrlParams = {
41
40
  /** The URL scheme for DWN discovery protocol handlers. */
42
41
  export const DWN_PROTOCOL_SCHEME = 'dwn';
43
42
 
44
- /** The `dwn://register` path that triggers the discovery handshake. */
45
- export const DWN_REGISTER_PATH = 'register';
43
+ /** The `dwn://connect` path that triggers the discovery handshake. */
44
+ export const DWN_CONNECT_PATH = 'connect';
46
45
 
47
46
  // ─── Register URL construction ───────────────────────────────────
48
47
 
49
48
  /**
50
- * Build a `dwn://register?callback=<url>` URL that, when opened by the OS,
49
+ * Build a `dwn://connect?callback=<url>` URL that, when opened by the OS,
51
50
  * triggers electrobun-dwn (or another `dwn://` scheme handler) to redirect
52
51
  * back to `callbackUrl` with the local DWN endpoint in the URL fragment.
53
52
  *
54
- * This is the **trigger** side of the `dwn://register` browser flow.
53
+ * This is the **trigger** side of the `dwn://connect` browser flow.
55
54
  * The web app opens this URL (e.g. via `window.open()` or `location.href`),
56
55
  * the OS routes it to the registered handler, and the handler redirects
57
56
  * back with the discovery payload.
@@ -59,17 +58,17 @@ export const DWN_REGISTER_PATH = 'register';
59
58
  * @param callbackUrl - The URL to redirect back to after discovery.
60
59
  * This should be the current page (or a dedicated callback page) that
61
60
  * will read the payload from `window.location.hash`.
62
- * @returns The `dwn://register?callback=<encoded-url>` URL string.
61
+ * @returns The `dwn://connect?callback=<encoded-url>` URL string.
63
62
  *
64
63
  * @example
65
64
  * ```ts
66
- * const registerUrl = buildDwnRegisterUrl('https://myapp.com/callback');
67
- * // => 'dwn://register?callback=https%3A%2F%2Fmyapp.com%2Fcallback'
65
+ * const registerUrl = buildDwnConnectUrl('https://myapp.com/callback');
66
+ * // => 'dwn://connect?callback=https%3A%2F%2Fmyapp.com%2Fcallback'
68
67
  * window.open(registerUrl);
69
68
  * ```
70
69
  */
71
- export function buildDwnRegisterUrl(callbackUrl: string): string {
72
- return `${DWN_PROTOCOL_SCHEME}://${DWN_REGISTER_PATH}?callback=${encodeURIComponent(callbackUrl)}`;
70
+ export function buildDwnConnectUrl(callbackUrl: string): string {
71
+ return `${DWN_PROTOCOL_SCHEME}://${DWN_CONNECT_PATH}?callback=${encodeURIComponent(callbackUrl)}`;
73
72
  }
74
73
 
75
74
  // ─── Payload encoding/decoding ───────────────────────────────────
@@ -110,15 +109,15 @@ export function decodeDwnDiscoveryPayload(encoded: string): DwnDiscoveryPayload
110
109
  // ─── URL parsing ─────────────────────────────────────────────────
111
110
 
112
111
  /**
113
- * Parse a `dwn://register?callback=<url>` URL into its components.
112
+ * Parse a `dwn://connect?callback=<url>` URL into its components.
114
113
  *
115
- * @param url - The full `dwn://register?callback=...` URL.
114
+ * @param url - The full `dwn://connect?callback=...` URL.
116
115
  * @returns The parsed parameters, or `undefined` if the URL is not a
117
- * valid `dwn://register` URL or is missing the `callback` parameter.
116
+ * valid `dwn://connect` URL or is missing the `callback` parameter.
118
117
  */
119
- export function parseDwnRegisterUrl(url: string): DwnRegisterUrlParams | undefined {
118
+ export function parseDwnConnectUrl(url: string): DwnConnectUrlParams | undefined {
120
119
  try {
121
- // dwn://register?callback=... is not a standard hierarchical URL, so
120
+ // dwn://connect?callback=... is not a standard hierarchical URL, so
122
121
  // we parse it manually to avoid URL constructor quirks with custom schemes.
123
122
  const schemePrefix = `${DWN_PROTOCOL_SCHEME}://`;
124
123
  if (!url.startsWith(schemePrefix)) {
@@ -134,7 +133,7 @@ export function parseDwnRegisterUrl(url: string): DwnRegisterUrlParams | undefin
134
133
  }
135
134
 
136
135
  const path = withoutScheme.slice(0, questionIndex);
137
- if (path !== DWN_REGISTER_PATH) {
136
+ if (path !== DWN_CONNECT_PATH) {
138
137
  return undefined;
139
138
  }
140
139
 
@@ -156,7 +155,7 @@ export function parseDwnRegisterUrl(url: string): DwnRegisterUrlParams | undefin
156
155
  * Build the full callback redirect URL with the discovery payload
157
156
  * encoded in the URL fragment.
158
157
  *
159
- * @param callbackUrl - The callback URL from the `dwn://register` request.
158
+ * @param callbackUrl - The callback URL from the `dwn://connect` request.
160
159
  * @param payload - The discovery payload to encode in the fragment.
161
160
  * @returns The full redirect URL (e.g. `https://notes.sh/dwn#eyJ...`).
162
161
  */
@@ -202,7 +201,7 @@ export function readDwnDiscoveryPayloadFromUrl(url: string): DwnDiscoveryPayload
202
201
  * Type guard for a valid {@link DwnDiscoveryPayload}.
203
202
  *
204
203
  * The endpoint MUST point to a loopback address (`127.0.0.1`, `[::1]`,
205
- * or `localhost`) because the `dwn://register` payload is only intended
204
+ * or `localhost`) because the `dwn://connect` payload is only intended
206
205
  * for local DWN discovery. Accepting arbitrary hostnames would allow a
207
206
  * malicious payload to redirect agent traffic to a remote server.
208
207
  *
@@ -231,7 +230,7 @@ function isValidPayload(value: unknown): value is DwnDiscoveryPayload {
231
230
  * address. Accepts `127.0.0.1`, `::1` (with or without brackets), and
232
231
  * `localhost` (bare or with any subdomain suffix, per RFC 6761 §6.3).
233
232
  *
234
- * This is a security boundary: the `dwn://register` redirect flow MUST
233
+ * This is a security boundary: the `dwn://connect` redirect flow MUST
235
234
  * NOT allow payloads that point to non-local servers.
236
235
  */
237
236
  function isLoopbackEndpoint(endpoint: string): boolean {
@@ -174,7 +174,7 @@ export async function writeContextKeyRecord(
174
174
  target : tenantDid,
175
175
  messageType : DwnInterface.RecordsWrite,
176
176
  messageParams : { ...contextKeyParams, dataCid, dataSize, encryptionInput },
177
- dataStream : new Blob([encryptedBytes]),
177
+ dataStream : new Blob([encryptedBytes as BlobPart]),
178
178
  }));
179
179
  } else {
180
180
  // --- Fallback: encrypt to the owner's key (local self-delivery) ---