@did-btcr2/method 0.29.0 → 0.32.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/README.md +13 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +8174 -7157
- package/dist/browser.mjs +8174 -7157
- package/dist/cjs/index.js +1845 -455
- package/dist/esm/core/aggregation/transport/factory.js +15 -6
- package/dist/esm/core/aggregation/transport/factory.js.map +1 -1
- package/dist/esm/core/aggregation/transport/http/client.js +350 -0
- package/dist/esm/core/aggregation/transport/http/client.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/envelope.js +126 -0
- package/dist/esm/core/aggregation/transport/http/envelope.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/errors.js +11 -0
- package/dist/esm/core/aggregation/transport/http/errors.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +45 -0
- package/dist/esm/core/aggregation/transport/http/inbox-buffer.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/index.js +12 -0
- package/dist/esm/core/aggregation/transport/http/index.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/nonce-cache.js +38 -0
- package/dist/esm/core/aggregation/transport/http/nonce-cache.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/protocol.js +28 -0
- package/dist/esm/core/aggregation/transport/http/protocol.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/rate-limiter.js +45 -0
- package/dist/esm/core/aggregation/transport/http/rate-limiter.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/request-auth.js +100 -0
- package/dist/esm/core/aggregation/transport/http/request-auth.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/server.js +481 -0
- package/dist/esm/core/aggregation/transport/http/server.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/sse-stream.js +110 -0
- package/dist/esm/core/aggregation/transport/http/sse-stream.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/sse-writer.js +25 -0
- package/dist/esm/core/aggregation/transport/http/sse-writer.js.map +1 -0
- package/dist/esm/core/aggregation/transport/index.js +1 -0
- package/dist/esm/core/aggregation/transport/index.js.map +1 -1
- package/dist/esm/core/beacon/beacon.js +197 -51
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/cas-beacon.js +3 -3
- package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
- package/dist/esm/core/beacon/singleton-beacon.js +3 -3
- package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
- package/dist/esm/core/beacon/smt-beacon.js +3 -3
- package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
- package/dist/esm/core/updater.js +63 -55
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/types/core/aggregation/transport/factory.d.ts +22 -7
- package/dist/types/core/aggregation/transport/factory.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/http/client.d.ts +48 -0
- package/dist/types/core/aggregation/transport/http/client.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/envelope.d.ts +64 -0
- package/dist/types/core/aggregation/transport/http/envelope.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/errors.d.ts +9 -0
- package/dist/types/core/aggregation/transport/http/errors.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +32 -0
- package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/index.d.ts +12 -0
- package/dist/types/core/aggregation/transport/http/index.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +26 -0
- package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/protocol.d.ts +53 -0
- package/dist/types/core/aggregation/transport/http/protocol.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +41 -0
- package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/request-auth.d.ts +50 -0
- package/dist/types/core/aggregation/transport/http/request-auth.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/server.d.ts +110 -0
- package/dist/types/core/aggregation/transport/http/server.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +34 -0
- package/dist/types/core/aggregation/transport/http/sse-stream.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +12 -0
- package/dist/types/core/aggregation/transport/http/sse-writer.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/index.d.ts +1 -0
- package/dist/types/core/aggregation/transport/index.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/transport.d.ts +1 -1
- package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +72 -12
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/cas-beacon.d.ts +3 -3
- package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/singleton-beacon.d.ts +3 -3
- package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/smt-beacon.d.ts +3 -3
- package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
- package/dist/types/core/updater.d.ts +27 -12
- package/dist/types/core/updater.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/core/aggregation/transport/factory.ts +48 -12
- package/src/core/aggregation/transport/http/client.ts +409 -0
- package/src/core/aggregation/transport/http/envelope.ts +204 -0
- package/src/core/aggregation/transport/http/errors.ts +11 -0
- package/src/core/aggregation/transport/http/inbox-buffer.ts +53 -0
- package/src/core/aggregation/transport/http/index.ts +11 -0
- package/src/core/aggregation/transport/http/nonce-cache.ts +43 -0
- package/src/core/aggregation/transport/http/protocol.ts +57 -0
- package/src/core/aggregation/transport/http/rate-limiter.ts +75 -0
- package/src/core/aggregation/transport/http/request-auth.ts +164 -0
- package/src/core/aggregation/transport/http/server.ts +615 -0
- package/src/core/aggregation/transport/http/sse-stream.ts +121 -0
- package/src/core/aggregation/transport/http/sse-writer.ts +23 -0
- package/src/core/aggregation/transport/index.ts +1 -0
- package/src/core/aggregation/transport/transport.ts +1 -1
- package/src/core/beacon/beacon.ts +255 -64
- package/src/core/beacon/cas-beacon.ts +4 -4
- package/src/core/beacon/singleton-beacon.ts +4 -4
- package/src/core/beacon/smt-beacon.ts +4 -4
- package/src/core/updater.ts +113 -67
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { BitcoinConnection } from '@did-btcr2/bitcoin';
|
|
2
|
-
import type {
|
|
2
|
+
import type { PatchOperation } from '@did-btcr2/common';
|
|
3
3
|
import { type SignedBTCR2Update, type UnsignedBTCR2Update } from '@did-btcr2/cryptosuite';
|
|
4
|
+
import type { Signer } from '@did-btcr2/keypair';
|
|
4
5
|
import { type Btcr2DidDocument, type DidVerificationMethod } from '../utils/did-document.js';
|
|
5
6
|
import type { BeaconService } from './beacon/interfaces.js';
|
|
6
7
|
/**
|
|
7
|
-
* The updater needs the caller to supply a
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The updater needs the caller to supply a {@link Signer} for the given
|
|
9
|
+
* verification method. The unsigned update is attached so the caller can
|
|
10
|
+
* inspect it before producing a signature. The signer can wrap a local secret
|
|
11
|
+
* key (`LocalSigner`), a KMS-managed key (`KeyManagerSigner`), or any custom backend.
|
|
10
12
|
*/
|
|
11
13
|
export interface NeedSigningKey {
|
|
12
14
|
readonly kind: 'NeedSigningKey';
|
|
@@ -29,6 +31,18 @@ export interface NeedFunding {
|
|
|
29
31
|
/** The beacon service this address belongs to. */
|
|
30
32
|
readonly beaconService: BeaconService;
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Optional proof the caller passes when fulfilling {@link NeedFunding}. The
|
|
36
|
+
* state machine asserts the proof before transitioning to Broadcast. Sans-I/O
|
|
37
|
+
* is preserved: the caller still performs the UTXO lookup; this is just a
|
|
38
|
+
* contract-level handshake.
|
|
39
|
+
*/
|
|
40
|
+
export interface FundingProof {
|
|
41
|
+
/** Number of spendable UTXOs the caller observed at the beacon address. Must be >= 1. */
|
|
42
|
+
utxoCount: number;
|
|
43
|
+
/** Optional txid the caller funded with, for diagnostics. */
|
|
44
|
+
txid?: string;
|
|
45
|
+
}
|
|
32
46
|
/**
|
|
33
47
|
* The updater needs the caller to broadcast the signed update via the beacon.
|
|
34
48
|
*
|
|
@@ -86,20 +100,21 @@ export interface UpdaterParams {
|
|
|
86
100
|
*
|
|
87
101
|
* ```typescript
|
|
88
102
|
* const updater = DidBtcr2.update({ sourceDocument, patches, ... });
|
|
103
|
+
* const signer = new LocalSigner(secretKeyBytes); // or KeyManagerSigner / custom
|
|
89
104
|
* let state = updater.advance();
|
|
90
105
|
*
|
|
91
106
|
* while(state.status === 'action-required') {
|
|
92
107
|
* for(const need of state.needs) {
|
|
93
108
|
* switch(need.kind) {
|
|
94
109
|
* case 'NeedSigningKey':
|
|
95
|
-
* updater.provide(need,
|
|
110
|
+
* updater.provide(need, signer);
|
|
96
111
|
* break;
|
|
97
112
|
* case 'NeedFunding':
|
|
98
113
|
* // Check UTXOs at need.beaconAddress, fund if needed
|
|
99
114
|
* updater.provide(need);
|
|
100
115
|
* break;
|
|
101
116
|
* case 'NeedBroadcast':
|
|
102
|
-
* await Updater.announce(need.beaconService, need.signedUpdate,
|
|
117
|
+
* await Updater.announce(need.beaconService, need.signedUpdate, signer, bitcoin);
|
|
103
118
|
* updater.provide(need);
|
|
104
119
|
* break;
|
|
105
120
|
* }
|
|
@@ -143,21 +158,21 @@ export declare class Updater {
|
|
|
143
158
|
* @param {string} did The did-btcr2 identifier to derive the root capability from.
|
|
144
159
|
* @param {UnsignedBTCR2Update} unsignedUpdate The unsigned update to sign.
|
|
145
160
|
* @param {DidVerificationMethod} verificationMethod The verification method for signing.
|
|
146
|
-
* @param {
|
|
161
|
+
* @param {Signer} signer Signer that produces the BIP-340 Schnorr signature.
|
|
147
162
|
* @returns {SignedBTCR2Update} The signed update with a Data Integrity proof.
|
|
148
163
|
*/
|
|
149
|
-
static sign(did: string, unsignedUpdate: UnsignedBTCR2Update, verificationMethod: DidVerificationMethod,
|
|
164
|
+
static sign(did: string, unsignedUpdate: UnsignedBTCR2Update, verificationMethod: DidVerificationMethod, signer: Signer): SignedBTCR2Update;
|
|
150
165
|
/**
|
|
151
166
|
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#announce-did-update | 7.3.d Announce DID Update}.
|
|
152
167
|
* Announces a signed update to the Bitcoin blockchain via the specified beacon.
|
|
153
168
|
*
|
|
154
169
|
* @param {BeaconService} beaconService The beacon service to broadcast through.
|
|
155
170
|
* @param {SignedBTCR2Update} update The signed update to announce.
|
|
156
|
-
* @param {
|
|
171
|
+
* @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
|
|
157
172
|
* @param {BitcoinConnection} bitcoin The Bitcoin network connection.
|
|
158
173
|
* @returns {Promise<SignedBTCR2Update>} The signed update that was broadcast.
|
|
159
174
|
*/
|
|
160
|
-
static announce(beaconService: BeaconService, update: SignedBTCR2Update,
|
|
175
|
+
static announce(beaconService: BeaconService, update: SignedBTCR2Update, signer: Signer, bitcoin: BitcoinConnection): Promise<SignedBTCR2Update>;
|
|
161
176
|
/**
|
|
162
177
|
* Advance the state machine. Returns either:
|
|
163
178
|
* - `{ status: 'action-required', needs }` — caller must provide data via {@link provide}
|
|
@@ -171,8 +186,8 @@ export declare class Updater {
|
|
|
171
186
|
* @param need The DataNeed being fulfilled (from the `needs` array).
|
|
172
187
|
* @param data The data payload corresponding to the need kind (omit for NeedFunding/NeedBroadcast).
|
|
173
188
|
*/
|
|
174
|
-
provide(need: NeedSigningKey, data:
|
|
175
|
-
provide(need: NeedFunding): void;
|
|
189
|
+
provide(need: NeedSigningKey, data: Signer): void;
|
|
190
|
+
provide(need: NeedFunding, proof?: FundingProof): void;
|
|
176
191
|
provide(need: NeedBroadcast): void;
|
|
177
192
|
}
|
|
178
193
|
//# sourceMappingURL=updater.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updater.d.ts","sourceRoot":"","sources":["../../../src/core/updater.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"updater.d.ts","sourceRoot":"","sources":["../../../src/core/updater.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@did-btcr2/method",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reference implementation for the did:btcr2 DID method written in TypeScript and JavaScript. did:btcr2 is a censorship resistant DID Method using the Bitcoin blockchain as a Verifiable Data Registry to announce changes to the DID document. This is the core method implementation for the did-btcr2-js monorepo.",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -80,11 +80,11 @@
|
|
|
80
80
|
"helia": "^5.5.1",
|
|
81
81
|
"multiformats": "^13.4.2",
|
|
82
82
|
"nostr-tools": "^2.23.3",
|
|
83
|
-
"@did-btcr2/smt": "^0.2.4",
|
|
84
|
-
"@did-btcr2/cryptosuite": "^6.0.6",
|
|
85
83
|
"@did-btcr2/bitcoin": "^0.6.0",
|
|
86
|
-
"@did-btcr2/common": "^9.
|
|
87
|
-
"@did-btcr2/
|
|
84
|
+
"@did-btcr2/common": "^9.1.0",
|
|
85
|
+
"@did-btcr2/cryptosuite": "^8.0.0",
|
|
86
|
+
"@did-btcr2/smt": "^0.2.4",
|
|
87
|
+
"@did-btcr2/keypair": "^0.13.0"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@eslint/js": "^9.39.4",
|
|
@@ -1,28 +1,64 @@
|
|
|
1
1
|
import { NotImplementedError } from '@did-btcr2/common';
|
|
2
|
-
import {
|
|
2
|
+
import type { Logger } from '../logger.js';
|
|
3
3
|
import { TransportError } from './error.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { HttpClientTransportConfig } from './http/client.js';
|
|
5
|
+
import { HttpClientTransport } from './http/client.js';
|
|
6
|
+
import type { HttpServerTransportConfig } from './http/server.js';
|
|
7
|
+
import { HttpServerTransport } from './http/server.js';
|
|
8
|
+
import { NostrTransport } from './nostr.js';
|
|
9
|
+
import type { Transport } from './transport.js';
|
|
10
|
+
|
|
11
|
+
/** Discriminated-union config for {@link TransportFactory.establish}. */
|
|
12
|
+
export type TransportConfig =
|
|
13
|
+
| NostrTransportConfigOption
|
|
14
|
+
| DidCommTransportConfigOption
|
|
15
|
+
| HttpClientTransportConfigOption
|
|
16
|
+
| HttpServerTransportConfigOption;
|
|
5
17
|
|
|
6
|
-
export interface
|
|
7
|
-
type:
|
|
18
|
+
export interface NostrTransportConfigOption {
|
|
19
|
+
type: 'nostr';
|
|
8
20
|
relays?: string[];
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
broadcastLookbackMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DidCommTransportConfigOption {
|
|
26
|
+
type: 'didcomm';
|
|
9
27
|
}
|
|
10
28
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
29
|
+
export interface HttpClientTransportConfigOption extends HttpClientTransportConfig {
|
|
30
|
+
type: 'http';
|
|
31
|
+
role: 'client';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HttpServerTransportConfigOption extends HttpServerTransportConfig {
|
|
35
|
+
type: 'http';
|
|
36
|
+
role: 'server';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Factory for creating Transport instances. */
|
|
15
40
|
export class TransportFactory {
|
|
16
41
|
static establish(config: TransportConfig): Transport {
|
|
17
|
-
switch
|
|
42
|
+
switch(config.type) {
|
|
18
43
|
case 'nostr':
|
|
19
|
-
return new NostrTransport({
|
|
44
|
+
return new NostrTransport({
|
|
45
|
+
relays : config.relays,
|
|
46
|
+
logger : config.logger,
|
|
47
|
+
broadcastLookbackMs : config.broadcastLookbackMs,
|
|
48
|
+
});
|
|
20
49
|
case 'didcomm':
|
|
21
50
|
throw new NotImplementedError('DIDComm transport not implemented yet.');
|
|
51
|
+
case 'http':
|
|
52
|
+
if(config.role === 'client') return new HttpClientTransport(config);
|
|
53
|
+
if(config.role === 'server') return new HttpServerTransport(config);
|
|
54
|
+
throw new NotImplementedError(
|
|
55
|
+
`HTTP transport role not implemented: ${(config as { role: string }).role}`,
|
|
56
|
+
);
|
|
22
57
|
default:
|
|
23
58
|
throw new TransportError(
|
|
24
|
-
`Invalid transport type: ${config.type}`,
|
|
25
|
-
'INVALID_TRANSPORT_TYPE',
|
|
59
|
+
`Invalid transport type: ${(config as { type: string }).type}`,
|
|
60
|
+
'INVALID_TRANSPORT_TYPE',
|
|
61
|
+
{ config },
|
|
26
62
|
);
|
|
27
63
|
}
|
|
28
64
|
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import type { SchnorrKeyPair } from '@did-btcr2/keypair';
|
|
2
|
+
import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
|
|
3
|
+
|
|
4
|
+
import { Identifier } from '../../../identifier.js';
|
|
5
|
+
import type { Logger } from '../../logger.js';
|
|
6
|
+
import { CONSOLE_LOGGER } from '../../logger.js';
|
|
7
|
+
import type { BaseMessage } from '../../messages/base.js';
|
|
8
|
+
import type { MessageHandler, Transport } from '../transport.js';
|
|
9
|
+
import { reviveFromWire, signEnvelope, verifyEnvelope } from './envelope.js';
|
|
10
|
+
import { HttpTransportError } from './errors.js';
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_CLOCK_SKEW_SEC,
|
|
13
|
+
HTTP_ROUTE,
|
|
14
|
+
SSE_EVENT,
|
|
15
|
+
type SignedEnvelope,
|
|
16
|
+
} from './protocol.js';
|
|
17
|
+
import { buildRequestAuth } from './request-auth.js';
|
|
18
|
+
import { parseSseStream } from './sse-stream.js';
|
|
19
|
+
|
|
20
|
+
export interface HttpClientTransportConfig {
|
|
21
|
+
/** Base URL of the aggregation service (e.g. `https://aggregator.example.com/`). */
|
|
22
|
+
baseUrl: string | URL;
|
|
23
|
+
/** Custom `fetch` implementation (tests, Workers, React Native). Defaults to `globalThis.fetch`. */
|
|
24
|
+
fetchImpl?: typeof fetch;
|
|
25
|
+
/** Diagnostic logger. Defaults to {@link CONSOLE_LOGGER}. */
|
|
26
|
+
logger?: Logger;
|
|
27
|
+
/** Reconnect backoff (ms) given attempt count (0-based). */
|
|
28
|
+
reconnectBackoff?: (attempt: number) => number;
|
|
29
|
+
/** Envelope / request-auth clock-skew tolerance in seconds. */
|
|
30
|
+
clockSkewSec?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Default exponential backoff: 1s, 2s, 4s, ..., capped at 30s, 20% jitter. */
|
|
34
|
+
export function defaultReconnectBackoff(attempt: number): number {
|
|
35
|
+
const base = Math.min(1000 * 2 ** attempt, 30_000);
|
|
36
|
+
const jitter = base * 0.2 * Math.random();
|
|
37
|
+
return Math.floor(base + jitter);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ActorEntry {
|
|
41
|
+
keys: SchnorrKeyPair;
|
|
42
|
+
handlers: Map<string, MessageHandler>;
|
|
43
|
+
inboxAbort?: AbortController;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* HTTP transport client. Implements the transport-agnostic {@link Transport}
|
|
48
|
+
* interface; the wire is fetch-based SSE for incoming events and fetch-based
|
|
49
|
+
* POST for outgoing messages. All runtime I/O goes through `fetchImpl` so
|
|
50
|
+
* tests can substitute a mock without touching the network.
|
|
51
|
+
*/
|
|
52
|
+
export class HttpClientTransport implements Transport {
|
|
53
|
+
readonly name = 'http';
|
|
54
|
+
|
|
55
|
+
readonly #baseUrl: URL;
|
|
56
|
+
readonly #fetch: typeof fetch;
|
|
57
|
+
readonly #logger: Logger;
|
|
58
|
+
readonly #backoff: (attempt: number) => number;
|
|
59
|
+
readonly #clockSkewSec: number;
|
|
60
|
+
|
|
61
|
+
readonly #actors: Map<string, ActorEntry> = new Map();
|
|
62
|
+
readonly #peers: Map<string, Uint8Array> = new Map();
|
|
63
|
+
|
|
64
|
+
#started = false;
|
|
65
|
+
#broadcastAbort?: AbortController;
|
|
66
|
+
|
|
67
|
+
constructor(config: HttpClientTransportConfig) {
|
|
68
|
+
const base = typeof config.baseUrl === 'string' ? new URL(config.baseUrl) : new URL(config.baseUrl.href);
|
|
69
|
+
if(!base.pathname.endsWith('/')) base.pathname += '/';
|
|
70
|
+
this.#baseUrl = base;
|
|
71
|
+
this.#logger = config.logger ?? CONSOLE_LOGGER;
|
|
72
|
+
this.#backoff = config.reconnectBackoff ?? defaultReconnectBackoff;
|
|
73
|
+
this.#clockSkewSec = config.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
|
|
74
|
+
|
|
75
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
76
|
+
if(typeof fetchImpl !== 'function') {
|
|
77
|
+
throw new HttpTransportError(
|
|
78
|
+
'No fetch implementation available. Pass config.fetchImpl explicitly.',
|
|
79
|
+
'NO_FETCH_IMPL',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
this.#fetch = fetchImpl;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
start(): void {
|
|
86
|
+
if(this.#started) return;
|
|
87
|
+
this.#started = true;
|
|
88
|
+
|
|
89
|
+
this.#broadcastAbort = new AbortController();
|
|
90
|
+
this.#runBroadcastLoop(this.#broadcastAbort.signal);
|
|
91
|
+
|
|
92
|
+
for(const [did, entry] of this.#actors) {
|
|
93
|
+
this.#openInbox(did, entry);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Tear down all SSE subscriptions and stop reconnect loops. Not part of the
|
|
99
|
+
* {@link Transport} interface, but needed in tests and whenever a client
|
|
100
|
+
* wants to cleanly disconnect without unregistering every actor.
|
|
101
|
+
*
|
|
102
|
+
* Idempotent. Actors remain registered (re-call {@link start} to resume).
|
|
103
|
+
*/
|
|
104
|
+
stop(): void {
|
|
105
|
+
this.#broadcastAbort?.abort();
|
|
106
|
+
this.#broadcastAbort = undefined;
|
|
107
|
+
for(const entry of this.#actors.values()) {
|
|
108
|
+
entry.inboxAbort?.abort();
|
|
109
|
+
entry.inboxAbort = undefined;
|
|
110
|
+
}
|
|
111
|
+
this.#started = false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
registerActor(did: string, keys: SchnorrKeyPair): void {
|
|
115
|
+
const existing = this.#actors.get(did);
|
|
116
|
+
if(existing?.inboxAbort) existing.inboxAbort.abort();
|
|
117
|
+
|
|
118
|
+
const entry: ActorEntry = { keys, handlers: new Map() };
|
|
119
|
+
this.#actors.set(did, entry);
|
|
120
|
+
if(this.#started) this.#openInbox(did, entry);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
unregisterActor(did: string): void {
|
|
124
|
+
const entry = this.#actors.get(did);
|
|
125
|
+
if(!entry) return;
|
|
126
|
+
entry.inboxAbort?.abort();
|
|
127
|
+
this.#actors.delete(did);
|
|
128
|
+
this.#peers.delete(did);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getActorPk(did: string): Uint8Array | undefined {
|
|
132
|
+
return this.#actors.get(did)?.keys.publicKey.compressed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
registerPeer(did: string, communicationPk: Uint8Array): void {
|
|
136
|
+
try {
|
|
137
|
+
new CompressedSecp256k1PublicKey(communicationPk);
|
|
138
|
+
} catch {
|
|
139
|
+
throw new HttpTransportError(
|
|
140
|
+
`Invalid communication public key for peer ${did}: expected a 33-byte compressed secp256k1 key.`,
|
|
141
|
+
'INVALID_PEER_KEY',
|
|
142
|
+
{ did, keyLength: communicationPk.length },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
this.#peers.set(did, communicationPk);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getPeerPk(did: string): Uint8Array | undefined {
|
|
149
|
+
return this.#peers.get(did);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
registerMessageHandler(actorDid: string, messageType: string, handler: MessageHandler): void {
|
|
153
|
+
const actor = this.#actors.get(actorDid);
|
|
154
|
+
if(!actor) {
|
|
155
|
+
throw new HttpTransportError(
|
|
156
|
+
`Cannot register handler: actor ${actorDid} not registered. Call registerActor() first.`,
|
|
157
|
+
'UNKNOWN_ACTOR',
|
|
158
|
+
{ did: actorDid },
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
actor.handlers.set(messageType, handler);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
unregisterMessageHandler(actorDid: string, messageType: string): void {
|
|
165
|
+
this.#actors.get(actorDid)?.handlers.delete(messageType);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async sendMessage(message: BaseMessage, sender: string, recipient?: string): Promise<void> {
|
|
169
|
+
const actor = this.#actors.get(sender);
|
|
170
|
+
if(!actor) {
|
|
171
|
+
throw new HttpTransportError(
|
|
172
|
+
`Unknown sender: ${sender}. Call registerActor() before sending messages.`,
|
|
173
|
+
'UNKNOWN_SENDER',
|
|
174
|
+
{ did: sender },
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const envelope = signEnvelope(
|
|
179
|
+
message,
|
|
180
|
+
{ did: sender, keys: actor.keys },
|
|
181
|
+
{ to: recipient },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const url = this.#route(HTTP_ROUTE.MESSAGES);
|
|
185
|
+
const res = await this.#fetch(url, {
|
|
186
|
+
method : 'POST',
|
|
187
|
+
headers : { 'content-type': 'application/json' },
|
|
188
|
+
body : JSON.stringify(envelope),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if(!res.ok) {
|
|
192
|
+
const body = await safeText(res);
|
|
193
|
+
throw new HttpTransportError(
|
|
194
|
+
`sendMessage failed: HTTP ${res.status}`,
|
|
195
|
+
'SEND_MESSAGE_HTTP',
|
|
196
|
+
{ status: res.status, body: body.slice(0, 256), messageType: message.type },
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
publishRepeating(
|
|
202
|
+
message: BaseMessage,
|
|
203
|
+
sender: string,
|
|
204
|
+
intervalMs: number,
|
|
205
|
+
recipient?: string,
|
|
206
|
+
): () => void {
|
|
207
|
+
let stopped = false;
|
|
208
|
+
const attempt = (): void => {
|
|
209
|
+
if(stopped) return;
|
|
210
|
+
this.sendMessage(message, sender, recipient).catch((err) => {
|
|
211
|
+
this.#logger.debug('publishRepeating send failed:', err);
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
attempt();
|
|
215
|
+
const timer = setInterval(attempt, intervalMs);
|
|
216
|
+
return (): void => {
|
|
217
|
+
if(stopped) return;
|
|
218
|
+
stopped = true;
|
|
219
|
+
clearInterval(timer);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#route(template: string): URL {
|
|
224
|
+
// Strip the leading slash so `new URL(rel, base)` is resolved against the
|
|
225
|
+
// base's pathname instead of replacing it.
|
|
226
|
+
return new URL(template.replace(/^\//, ''), this.#baseUrl);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#openInbox(did: string, entry: ActorEntry): void {
|
|
230
|
+
const abort = new AbortController();
|
|
231
|
+
entry.inboxAbort = abort;
|
|
232
|
+
this.#runInboxLoop(did, entry, abort.signal);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async #runBroadcastLoop(signal: AbortSignal): Promise<void> {
|
|
236
|
+
const url = this.#route(HTTP_ROUTE.ADVERTS);
|
|
237
|
+
let attempt = 0;
|
|
238
|
+
while(!signal.aborted) {
|
|
239
|
+
try {
|
|
240
|
+
const res = await this.#fetch(url, {
|
|
241
|
+
method : 'GET',
|
|
242
|
+
headers : { accept: 'text/event-stream' },
|
|
243
|
+
signal,
|
|
244
|
+
});
|
|
245
|
+
if(!res.ok || !res.body) {
|
|
246
|
+
this.#logger.warn(`Broadcast subscribe failed: HTTP ${res.status}`);
|
|
247
|
+
await sleep(this.#backoff(attempt++), signal);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
attempt = 0;
|
|
251
|
+
for await (const ev of parseSseStream(res.body)) {
|
|
252
|
+
if(signal.aborted) return;
|
|
253
|
+
if(ev.event !== SSE_EVENT.ADVERT) continue;
|
|
254
|
+
this.#dispatchBroadcast(ev.data);
|
|
255
|
+
}
|
|
256
|
+
} catch(err) {
|
|
257
|
+
if(signal.aborted) return;
|
|
258
|
+
this.#logger.debug('Broadcast loop error:', err);
|
|
259
|
+
try {
|
|
260
|
+
await sleep(this.#backoff(attempt++), signal);
|
|
261
|
+
} catch {
|
|
262
|
+
return; // sleep was aborted
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async #runInboxLoop(did: string, entry: ActorEntry, signal: AbortSignal): Promise<void> {
|
|
269
|
+
const url = this.#route(HTTP_ROUTE.ACTOR_INBOX.replace('{did}', encodeURIComponent(did)));
|
|
270
|
+
let attempt = 0;
|
|
271
|
+
while(!signal.aborted) {
|
|
272
|
+
try {
|
|
273
|
+
const auth = buildRequestAuth(did, entry.keys, url.pathname);
|
|
274
|
+
const res = await this.#fetch(url, {
|
|
275
|
+
method : 'GET',
|
|
276
|
+
headers : { accept: 'text/event-stream', authorization: auth },
|
|
277
|
+
signal,
|
|
278
|
+
});
|
|
279
|
+
if(!res.ok || !res.body) {
|
|
280
|
+
this.#logger.warn(`Inbox subscribe failed for ${did}: HTTP ${res.status}`);
|
|
281
|
+
await sleep(this.#backoff(attempt++), signal);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
attempt = 0;
|
|
285
|
+
for await (const ev of parseSseStream(res.body)) {
|
|
286
|
+
if(signal.aborted) return;
|
|
287
|
+
if(ev.event !== SSE_EVENT.MESSAGE) continue;
|
|
288
|
+
await this.#dispatchInbox(ev.data, did, entry);
|
|
289
|
+
}
|
|
290
|
+
} catch(err) {
|
|
291
|
+
if(signal.aborted) return;
|
|
292
|
+
this.#logger.debug(`Inbox loop error for ${did}:`, err);
|
|
293
|
+
try {
|
|
294
|
+
await sleep(this.#backoff(attempt++), signal);
|
|
295
|
+
} catch {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#dispatchBroadcast(dataJson: string): void {
|
|
303
|
+
const envelope = parseEnvelope(dataJson, this.#logger);
|
|
304
|
+
if(!envelope) return;
|
|
305
|
+
|
|
306
|
+
const senderPk = this.#resolveSenderPk(envelope.from);
|
|
307
|
+
if(!senderPk) {
|
|
308
|
+
this.#logger.debug(`Broadcast from unresolvable DID: ${envelope.from}`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
|
|
313
|
+
} catch(err) {
|
|
314
|
+
this.#logger.debug('Broadcast envelope verification failed:', err);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const revived = reviveFromWire(envelope.message) as Record<string, unknown>;
|
|
319
|
+
const flat = flattenMessage(revived);
|
|
320
|
+
const messageType = typeof flat.type === 'string' ? flat.type : undefined;
|
|
321
|
+
if(!messageType) return;
|
|
322
|
+
|
|
323
|
+
for(const actor of this.#actors.values()) {
|
|
324
|
+
const handler = actor.handlers.get(messageType);
|
|
325
|
+
if(handler) void Promise.resolve(handler(flat));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async #dispatchInbox(dataJson: string, actorDid: string, entry: ActorEntry): Promise<void> {
|
|
330
|
+
const envelope = parseEnvelope(dataJson, this.#logger);
|
|
331
|
+
if(!envelope) return;
|
|
332
|
+
|
|
333
|
+
const senderPk = this.#resolveSenderPk(envelope.from);
|
|
334
|
+
if(!senderPk) {
|
|
335
|
+
this.#logger.debug(`Inbox message from unresolvable DID: ${envelope.from}`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
verifyEnvelope(envelope, senderPk, {
|
|
340
|
+
clockSkewSec : this.#clockSkewSec,
|
|
341
|
+
expectedTo : actorDid,
|
|
342
|
+
});
|
|
343
|
+
} catch(err) {
|
|
344
|
+
this.#logger.debug(`Inbox envelope verification failed for ${actorDid}:`, err);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const revived = reviveFromWire(envelope.message) as Record<string, unknown>;
|
|
349
|
+
const flat = flattenMessage(revived);
|
|
350
|
+
const messageType = typeof flat.type === 'string' ? flat.type : undefined;
|
|
351
|
+
if(!messageType) return;
|
|
352
|
+
|
|
353
|
+
const handler = entry.handlers.get(messageType);
|
|
354
|
+
if(handler) await handler(flat);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#resolveSenderPk(did: string): CompressedSecp256k1PublicKey | undefined {
|
|
358
|
+
const peerBytes = this.#peers.get(did);
|
|
359
|
+
if(peerBytes) {
|
|
360
|
+
try { return new CompressedSecp256k1PublicKey(peerBytes); }
|
|
361
|
+
catch { /* fall through to DID decode */ }
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const components = Identifier.decode(did);
|
|
365
|
+
if(components.idType === 'KEY') {
|
|
366
|
+
return new CompressedSecp256k1PublicKey(components.genesisBytes);
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
// Not a decodable did:btcr2 KEY identifier.
|
|
370
|
+
}
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
376
|
+
if(ms <= 0) return Promise.resolve();
|
|
377
|
+
return new Promise((resolve, reject) => {
|
|
378
|
+
const onAbort = (): void => {
|
|
379
|
+
clearTimeout(timer);
|
|
380
|
+
reject(new Error('aborted'));
|
|
381
|
+
};
|
|
382
|
+
const timer = setTimeout(() => {
|
|
383
|
+
signal.removeEventListener('abort', onAbort);
|
|
384
|
+
resolve();
|
|
385
|
+
}, ms);
|
|
386
|
+
if(signal.aborted) onAbort();
|
|
387
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function safeText(res: Response): Promise<string> {
|
|
392
|
+
try { return await res.text(); }
|
|
393
|
+
catch { return ''; }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function parseEnvelope(dataJson: string, logger: Logger): SignedEnvelope | undefined {
|
|
397
|
+
try { return JSON.parse(dataJson) as SignedEnvelope; }
|
|
398
|
+
catch(err) {
|
|
399
|
+
logger.debug('SSE event: failed to parse envelope JSON:', err);
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function flattenMessage(msg: Record<string, unknown>): Record<string, unknown> {
|
|
405
|
+
if(msg.body && typeof msg.body === 'object') {
|
|
406
|
+
return { ...msg, ...(msg.body as Record<string, unknown>) };
|
|
407
|
+
}
|
|
408
|
+
return msg;
|
|
409
|
+
}
|