@did-btcr2/method 0.28.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.
Files changed (193) hide show
  1. package/README.md +13 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.js +34125 -44647
  4. package/dist/browser.mjs +26409 -36931
  5. package/dist/cjs/index.js +2869 -679
  6. package/dist/esm/core/aggregation/beacon-strategy.js +62 -0
  7. package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -0
  8. package/dist/esm/core/aggregation/cohort.js +31 -8
  9. package/dist/esm/core/aggregation/cohort.js.map +1 -1
  10. package/dist/esm/core/aggregation/logger.js +15 -0
  11. package/dist/esm/core/aggregation/logger.js.map +1 -0
  12. package/dist/esm/core/aggregation/messages/base.js +12 -1
  13. package/dist/esm/core/aggregation/messages/base.js.map +1 -1
  14. package/dist/esm/core/aggregation/messages/bodies.js +90 -0
  15. package/dist/esm/core/aggregation/messages/bodies.js.map +1 -0
  16. package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
  17. package/dist/esm/core/aggregation/messages/index.js +1 -0
  18. package/dist/esm/core/aggregation/messages/index.js.map +1 -1
  19. package/dist/esm/core/aggregation/participant.js +39 -46
  20. package/dist/esm/core/aggregation/participant.js.map +1 -1
  21. package/dist/esm/core/aggregation/runner/participant-runner.js +33 -7
  22. package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
  23. package/dist/esm/core/aggregation/runner/service-runner.js +198 -19
  24. package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
  25. package/dist/esm/core/aggregation/service.js +143 -15
  26. package/dist/esm/core/aggregation/service.js.map +1 -1
  27. package/dist/esm/core/aggregation/signing-session.js +44 -5
  28. package/dist/esm/core/aggregation/signing-session.js.map +1 -1
  29. package/dist/esm/core/aggregation/transport/didcomm.js +9 -0
  30. package/dist/esm/core/aggregation/transport/didcomm.js.map +1 -1
  31. package/dist/esm/core/aggregation/transport/factory.js +15 -6
  32. package/dist/esm/core/aggregation/transport/factory.js.map +1 -1
  33. package/dist/esm/core/aggregation/transport/http/client.js +350 -0
  34. package/dist/esm/core/aggregation/transport/http/client.js.map +1 -0
  35. package/dist/esm/core/aggregation/transport/http/envelope.js +126 -0
  36. package/dist/esm/core/aggregation/transport/http/envelope.js.map +1 -0
  37. package/dist/esm/core/aggregation/transport/http/errors.js +11 -0
  38. package/dist/esm/core/aggregation/transport/http/errors.js.map +1 -0
  39. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +45 -0
  40. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js.map +1 -0
  41. package/dist/esm/core/aggregation/transport/http/index.js +12 -0
  42. package/dist/esm/core/aggregation/transport/http/index.js.map +1 -0
  43. package/dist/esm/core/aggregation/transport/http/nonce-cache.js +38 -0
  44. package/dist/esm/core/aggregation/transport/http/nonce-cache.js.map +1 -0
  45. package/dist/esm/core/aggregation/transport/http/protocol.js +28 -0
  46. package/dist/esm/core/aggregation/transport/http/protocol.js.map +1 -0
  47. package/dist/esm/core/aggregation/transport/http/rate-limiter.js +45 -0
  48. package/dist/esm/core/aggregation/transport/http/rate-limiter.js.map +1 -0
  49. package/dist/esm/core/aggregation/transport/http/request-auth.js +100 -0
  50. package/dist/esm/core/aggregation/transport/http/request-auth.js.map +1 -0
  51. package/dist/esm/core/aggregation/transport/http/server.js +481 -0
  52. package/dist/esm/core/aggregation/transport/http/server.js.map +1 -0
  53. package/dist/esm/core/aggregation/transport/http/sse-stream.js +110 -0
  54. package/dist/esm/core/aggregation/transport/http/sse-stream.js.map +1 -0
  55. package/dist/esm/core/aggregation/transport/http/sse-writer.js +25 -0
  56. package/dist/esm/core/aggregation/transport/http/sse-writer.js.map +1 -0
  57. package/dist/esm/core/aggregation/transport/index.js +1 -0
  58. package/dist/esm/core/aggregation/transport/index.js.map +1 -1
  59. package/dist/esm/core/aggregation/transport/nostr.js +245 -16
  60. package/dist/esm/core/aggregation/transport/nostr.js.map +1 -1
  61. package/dist/esm/core/beacon/beacon.js +295 -63
  62. package/dist/esm/core/beacon/beacon.js.map +1 -1
  63. package/dist/esm/core/beacon/cas-beacon.js +3 -3
  64. package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
  65. package/dist/esm/core/beacon/singleton-beacon.js +3 -3
  66. package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
  67. package/dist/esm/core/beacon/smt-beacon.js +3 -3
  68. package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
  69. package/dist/esm/core/beacon/utils.js +14 -9
  70. package/dist/esm/core/beacon/utils.js.map +1 -1
  71. package/dist/esm/core/updater.js +63 -55
  72. package/dist/esm/core/updater.js.map +1 -1
  73. package/dist/esm/did-btcr2.js +0 -4
  74. package/dist/esm/did-btcr2.js.map +1 -1
  75. package/dist/esm/index.js +2 -0
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/esm/utils/did-document.js +2 -2
  78. package/dist/esm/utils/did-document.js.map +1 -1
  79. package/dist/types/core/aggregation/beacon-strategy.d.ts +52 -0
  80. package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -0
  81. package/dist/types/core/aggregation/cohort.d.ts +20 -3
  82. package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
  83. package/dist/types/core/aggregation/logger.d.ts +22 -0
  84. package/dist/types/core/aggregation/logger.d.ts.map +1 -0
  85. package/dist/types/core/aggregation/messages/base.d.ts +13 -1
  86. package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
  87. package/dist/types/core/aggregation/messages/bodies.d.ts +130 -0
  88. package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -0
  89. package/dist/types/core/aggregation/messages/factories.d.ts +1 -0
  90. package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
  91. package/dist/types/core/aggregation/messages/index.d.ts +1 -0
  92. package/dist/types/core/aggregation/messages/index.d.ts.map +1 -1
  93. package/dist/types/core/aggregation/participant.d.ts +2 -0
  94. package/dist/types/core/aggregation/participant.d.ts.map +1 -1
  95. package/dist/types/core/aggregation/runner/events.d.ts +32 -6
  96. package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
  97. package/dist/types/core/aggregation/runner/participant-runner.d.ts +7 -5
  98. package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
  99. package/dist/types/core/aggregation/runner/service-runner.d.ts +33 -3
  100. package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
  101. package/dist/types/core/aggregation/service.d.ts +33 -2
  102. package/dist/types/core/aggregation/service.d.ts.map +1 -1
  103. package/dist/types/core/aggregation/signing-session.d.ts +5 -1
  104. package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
  105. package/dist/types/core/aggregation/transport/didcomm.d.ts +3 -0
  106. package/dist/types/core/aggregation/transport/didcomm.d.ts.map +1 -1
  107. package/dist/types/core/aggregation/transport/factory.d.ts +22 -7
  108. package/dist/types/core/aggregation/transport/factory.d.ts.map +1 -1
  109. package/dist/types/core/aggregation/transport/http/client.d.ts +48 -0
  110. package/dist/types/core/aggregation/transport/http/client.d.ts.map +1 -0
  111. package/dist/types/core/aggregation/transport/http/envelope.d.ts +64 -0
  112. package/dist/types/core/aggregation/transport/http/envelope.d.ts.map +1 -0
  113. package/dist/types/core/aggregation/transport/http/errors.d.ts +9 -0
  114. package/dist/types/core/aggregation/transport/http/errors.d.ts.map +1 -0
  115. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +32 -0
  116. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts.map +1 -0
  117. package/dist/types/core/aggregation/transport/http/index.d.ts +12 -0
  118. package/dist/types/core/aggregation/transport/http/index.d.ts.map +1 -0
  119. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +26 -0
  120. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts.map +1 -0
  121. package/dist/types/core/aggregation/transport/http/protocol.d.ts +53 -0
  122. package/dist/types/core/aggregation/transport/http/protocol.d.ts.map +1 -0
  123. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +41 -0
  124. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts.map +1 -0
  125. package/dist/types/core/aggregation/transport/http/request-auth.d.ts +50 -0
  126. package/dist/types/core/aggregation/transport/http/request-auth.d.ts.map +1 -0
  127. package/dist/types/core/aggregation/transport/http/server.d.ts +110 -0
  128. package/dist/types/core/aggregation/transport/http/server.d.ts.map +1 -0
  129. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +34 -0
  130. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts.map +1 -0
  131. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +12 -0
  132. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts.map +1 -0
  133. package/dist/types/core/aggregation/transport/index.d.ts +1 -0
  134. package/dist/types/core/aggregation/transport/index.d.ts.map +1 -1
  135. package/dist/types/core/aggregation/transport/nostr.d.ts +99 -1
  136. package/dist/types/core/aggregation/transport/nostr.d.ts.map +1 -1
  137. package/dist/types/core/aggregation/transport/transport.d.ts +26 -1
  138. package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
  139. package/dist/types/core/beacon/beacon.d.ts +149 -22
  140. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  141. package/dist/types/core/beacon/cas-beacon.d.ts +3 -3
  142. package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
  143. package/dist/types/core/beacon/singleton-beacon.d.ts +3 -3
  144. package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
  145. package/dist/types/core/beacon/smt-beacon.d.ts +3 -3
  146. package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
  147. package/dist/types/core/beacon/utils.d.ts +2 -2
  148. package/dist/types/core/beacon/utils.d.ts.map +1 -1
  149. package/dist/types/core/updater.d.ts +27 -12
  150. package/dist/types/core/updater.d.ts.map +1 -1
  151. package/dist/types/did-btcr2.d.ts.map +1 -1
  152. package/dist/types/index.d.ts +2 -0
  153. package/dist/types/index.d.ts.map +1 -1
  154. package/package.json +5 -7
  155. package/src/core/aggregation/beacon-strategy.ts +123 -0
  156. package/src/core/aggregation/cohort.ts +34 -8
  157. package/src/core/aggregation/logger.ts +33 -0
  158. package/src/core/aggregation/messages/base.ts +20 -5
  159. package/src/core/aggregation/messages/bodies.ts +223 -0
  160. package/src/core/aggregation/messages/factories.ts +1 -0
  161. package/src/core/aggregation/messages/index.ts +1 -0
  162. package/src/core/aggregation/participant.ts +40 -46
  163. package/src/core/aggregation/runner/events.ts +27 -3
  164. package/src/core/aggregation/runner/participant-runner.ts +41 -7
  165. package/src/core/aggregation/runner/service-runner.ts +227 -19
  166. package/src/core/aggregation/service.ts +189 -20
  167. package/src/core/aggregation/signing-session.ts +65 -7
  168. package/src/core/aggregation/transport/didcomm.ts +17 -0
  169. package/src/core/aggregation/transport/factory.ts +48 -12
  170. package/src/core/aggregation/transport/http/client.ts +409 -0
  171. package/src/core/aggregation/transport/http/envelope.ts +204 -0
  172. package/src/core/aggregation/transport/http/errors.ts +11 -0
  173. package/src/core/aggregation/transport/http/inbox-buffer.ts +53 -0
  174. package/src/core/aggregation/transport/http/index.ts +11 -0
  175. package/src/core/aggregation/transport/http/nonce-cache.ts +43 -0
  176. package/src/core/aggregation/transport/http/protocol.ts +57 -0
  177. package/src/core/aggregation/transport/http/rate-limiter.ts +75 -0
  178. package/src/core/aggregation/transport/http/request-auth.ts +164 -0
  179. package/src/core/aggregation/transport/http/server.ts +615 -0
  180. package/src/core/aggregation/transport/http/sse-stream.ts +121 -0
  181. package/src/core/aggregation/transport/http/sse-writer.ts +23 -0
  182. package/src/core/aggregation/transport/index.ts +1 -0
  183. package/src/core/aggregation/transport/nostr.ts +266 -23
  184. package/src/core/aggregation/transport/transport.ts +34 -1
  185. package/src/core/beacon/beacon.ts +411 -79
  186. package/src/core/beacon/cas-beacon.ts +4 -4
  187. package/src/core/beacon/singleton-beacon.ts +4 -4
  188. package/src/core/beacon/smt-beacon.ts +4 -4
  189. package/src/core/beacon/utils.ts +16 -11
  190. package/src/core/updater.ts +113 -67
  191. package/src/did-btcr2.ts +0 -5
  192. package/src/index.ts +2 -0
  193. package/src/utils/did-document.ts +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../../../../src/core/aggregation/transport/http/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,OAAO,EAAO,aAAa;IAC3B,QAAQ,EAAM,cAAc;IAC5B,WAAW,EAAG,wBAAwB;IACtC,UAAU,EAAI,6BAA6B;CACnC,CAAC;AAEX,4EAA4E;AAC5E,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,MAAM,EAAM,QAAQ;IACpB,OAAO,EAAK,SAAS;IACrB,SAAS,EAAG,WAAW;CACf,CAAC;AAEX,sEAAsE;AACtE,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC,oEAAoE;AACpE,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC"}
@@ -0,0 +1,45 @@
1
+ export class InMemoryRateLimitStore {
2
+ #buckets = new Map();
3
+ get(key) {
4
+ return this.#buckets.get(key);
5
+ }
6
+ set(key, state) {
7
+ this.#buckets.set(key, state);
8
+ }
9
+ }
10
+ /**
11
+ * Token-bucket rate limiter keyed by an opaque string (typically a verified
12
+ * sender DID). Tokens refill linearly at `rps` up to `burst`. Each `consume`
13
+ * call atomically debits one token or returns `false` to reject.
14
+ *
15
+ * The limiter is synchronous and deterministic given `nowMs` — tests can
16
+ * drive it with a fixed clock to exercise exact boundaries.
17
+ */
18
+ export class RateLimiter {
19
+ #rps;
20
+ #burst;
21
+ #store;
22
+ constructor(config = {}) {
23
+ this.#rps = config.rps ?? 10;
24
+ this.#burst = config.burst ?? 30;
25
+ this.#store = config.store ?? new InMemoryRateLimitStore();
26
+ }
27
+ /** Consume one token for `key`. Returns `true` if accepted, `false` if throttled. */
28
+ consume(key, nowMs) {
29
+ const existing = this.#store.get(key);
30
+ const state = existing ?? { tokens: this.#burst, lastRefillMs: nowMs };
31
+ if (existing) {
32
+ const elapsedSec = Math.max(0, (nowMs - existing.lastRefillMs) / 1000);
33
+ state.tokens = Math.min(this.#burst, existing.tokens + elapsedSec * this.#rps);
34
+ state.lastRefillMs = nowMs;
35
+ }
36
+ if (state.tokens < 1) {
37
+ this.#store.set(key, state);
38
+ return false;
39
+ }
40
+ state.tokens -= 1;
41
+ this.#store.set(key, state);
42
+ return true;
43
+ }
44
+ }
45
+ //# sourceMappingURL=rate-limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../../../../../src/core/aggregation/transport/http/rate-limiter.ts"],"names":[],"mappings":"AAeA,MAAM,OAAO,sBAAsB;IACxB,QAAQ,GAAG,IAAI,GAAG,EAAuB,CAAC;IAEnD,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAkB;QACjC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;CACF;AAWD;;;;;;;GAOG;AACH,MAAM,OAAO,WAAW;IACb,IAAI,CAAS;IACb,MAAM,CAAS;IACf,MAAM,CAAiB;IAEhC,YAAY,SAA4B,EAAE;QACxC,IAAI,CAAC,IAAI,GAAK,MAAM,CAAC,GAAG,IAAM,EAAE,CAAC;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,IAAI,IAAI,sBAAsB,EAAE,CAAC;IAC7D,CAAC;IAED,qFAAqF;IACrF,OAAO,CAAC,GAAW,EAAE,KAAa;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,KAAK,GAAgB,QAAQ,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;QAEpF,IAAG,QAAQ,EAAE,CAAC;YACZ,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;YACvE,KAAK,CAAC,MAAM,GAAS,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;YACrF,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;QAC7B,CAAC;QAED,IAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC5B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;QAClB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
@@ -0,0 +1,100 @@
1
+ import { canonicalHashBytes } from '@did-btcr2/common';
2
+ import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils';
3
+ import { HttpTransportError } from './errors.js';
4
+ import { DEFAULT_CLOCK_SKEW_SEC, DEFAULT_NONCE_LEN_BYTES, HTTP_ENVELOPE_VERSION } from './protocol.js';
5
+ /**
6
+ * `Authorization`-header scheme used to authenticate SSE subscription
7
+ * requests. The header value takes the form
8
+ * `BTCR2-Sig v=<n>,did=<did>,ts=<unix>,nonce=<hex>,sig=<hex>`.
9
+ *
10
+ * Used only for GET endpoints (SSE inbox subscribe). POST endpoints carry a
11
+ * full {@link SignedEnvelope} in the request body instead.
12
+ */
13
+ export const REQUEST_AUTH_SCHEME = 'BTCR2-Sig';
14
+ /**
15
+ * Build an `Authorization` header value proving the caller controls the
16
+ * private key for `did`. Covers a specific request path so the signature
17
+ * can't be replayed against a different endpoint.
18
+ */
19
+ export function buildRequestAuth(did, keys, path, opts = {}) {
20
+ const ts = opts.timestamp ?? Math.floor(Date.now() / 1000);
21
+ const nonce = opts.nonce ?? bytesToHex(randomBytes(DEFAULT_NONCE_LEN_BYTES));
22
+ const digest = canonicalHashBytes({
23
+ v: HTTP_ENVELOPE_VERSION,
24
+ did,
25
+ ts,
26
+ nonce,
27
+ path,
28
+ });
29
+ const sig = keys.secretKey.sign(digest, { scheme: 'schnorr' });
30
+ return `${REQUEST_AUTH_SCHEME} v=${HTTP_ENVELOPE_VERSION},did=${did},ts=${ts},nonce=${nonce},sig=${bytesToHex(sig)}`;
31
+ }
32
+ /**
33
+ * Parse a `BTCR2-Sig` auth header value into its structured fields. Does NOT
34
+ * verify the signature — call {@link verifyRequestAuth} for that.
35
+ */
36
+ export function parseRequestAuth(headerValue) {
37
+ const prefix = `${REQUEST_AUTH_SCHEME} `;
38
+ if (!headerValue.startsWith(prefix)) {
39
+ throw new HttpTransportError(`Unexpected auth scheme (want ${REQUEST_AUTH_SCHEME})`, 'REQUEST_AUTH_SCHEME');
40
+ }
41
+ const params = {};
42
+ for (const piece of headerValue.slice(prefix.length).split(',')) {
43
+ const eq = piece.indexOf('=');
44
+ if (eq === -1)
45
+ continue;
46
+ const key = piece.slice(0, eq).trim();
47
+ const val = piece.slice(eq + 1).trim();
48
+ if (key.length > 0)
49
+ params[key] = val;
50
+ }
51
+ const v = Number(params.v);
52
+ const ts = Number(params.ts);
53
+ if (!Number.isInteger(v) || !Number.isInteger(ts) || !params.did || !params.nonce || !params.sig) {
54
+ throw new HttpTransportError('Malformed auth header (missing or invalid field)', 'REQUEST_AUTH_MALFORMED', { received: Object.keys(params) });
55
+ }
56
+ return { v, did: params.did, ts, nonce: params.nonce, sig: params.sig };
57
+ }
58
+ /**
59
+ * Parse + verify an auth header. Throws {@link HttpTransportError} on any
60
+ * failure; returns the parsed fields on success.
61
+ *
62
+ * `expectedPath` must match the path the signature commits to. `senderPk`
63
+ * must correspond to the DID the signer claims.
64
+ */
65
+ export function verifyRequestAuth(headerValue, expectedPath, senderPk, opts = {}) {
66
+ const parsed = parseRequestAuth(headerValue);
67
+ if (parsed.v !== HTTP_ENVELOPE_VERSION) {
68
+ throw new HttpTransportError(`Unsupported auth version: ${parsed.v}`, 'REQUEST_AUTH_VERSION_MISMATCH', { version: parsed.v, expected: HTTP_ENVELOPE_VERSION });
69
+ }
70
+ const skewSec = opts.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
71
+ const nowMs = opts.now ? opts.now() : Date.now();
72
+ const nowSec = Math.floor(nowMs / 1000);
73
+ const diff = Math.abs(nowSec - parsed.ts);
74
+ if (diff > skewSec) {
75
+ throw new HttpTransportError(`Auth timestamp out of skew: ${diff}s > ${skewSec}s`, 'REQUEST_AUTH_TIMESTAMP_SKEW', { diff, skewSec });
76
+ }
77
+ let sigBytes;
78
+ try {
79
+ sigBytes = hexToBytes(parsed.sig);
80
+ }
81
+ catch {
82
+ throw new HttpTransportError('Auth signature is not valid hex', 'REQUEST_AUTH_SIG_HEX');
83
+ }
84
+ if (sigBytes.length !== 64) {
85
+ throw new HttpTransportError(`Invalid auth signature length: ${sigBytes.length}`, 'REQUEST_AUTH_SIG_LENGTH', { length: sigBytes.length });
86
+ }
87
+ const digest = canonicalHashBytes({
88
+ v: parsed.v,
89
+ did: parsed.did,
90
+ ts: parsed.ts,
91
+ nonce: parsed.nonce,
92
+ path: expectedPath,
93
+ });
94
+ const ok = senderPk.verify(sigBytes, digest, { scheme: 'schnorr' });
95
+ if (!ok) {
96
+ throw new HttpTransportError('Auth signature verification failed', 'REQUEST_AUTH_SIG_INVALID');
97
+ }
98
+ return parsed;
99
+ }
100
+ //# sourceMappingURL=request-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-auth.js","sourceRoot":"","sources":["../../../../../../src/core/aggregation/transport/http/request-auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,481 @@
1
+ import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
2
+ import { Identifier } from '../../../identifier.js';
3
+ import { CONSOLE_LOGGER } from '../../logger.js';
4
+ import { reviveFromWire, signEnvelope, verifyEnvelope } from './envelope.js';
5
+ import { HttpTransportError } from './errors.js';
6
+ import { InboxBuffer } from './inbox-buffer.js';
7
+ import { NonceCache } from './nonce-cache.js';
8
+ import { DEFAULT_CLOCK_SKEW_SEC, HTTP_ENVELOPE_VERSION, HTTP_ROUTE, SSE_EVENT, } from './protocol.js';
9
+ import { RateLimiter } from './rate-limiter.js';
10
+ import { verifyRequestAuth } from './request-auth.js';
11
+ const INBOX_PATH_PREFIX = '/v1/actors/';
12
+ const INBOX_PATH_SUFFIX = '/inbox';
13
+ const DEFAULT_ADVERT_TTL_MS = 5 * 60 * 1000;
14
+ const DEFAULT_HEARTBEAT_MS = 20_000;
15
+ /**
16
+ * Server-side HTTP transport. Sans-I/O — the caller mounts
17
+ * {@link handleRequest} and {@link handleSse} under their HTTP framework of
18
+ * choice; the transport owns only in-memory state (actors, inboxes, advert
19
+ * cache, replay / rate-limit policies).
20
+ *
21
+ * Implements the generic {@link Transport} interface so the aggregation
22
+ * runners can drive it exactly the same way they drive {@link NostrTransport}
23
+ * or {@link HttpClientTransport}.
24
+ */
25
+ export class HttpServerTransport {
26
+ name = 'http';
27
+ #logger;
28
+ #cors;
29
+ #clockSkewSec;
30
+ #inboxBufferSize;
31
+ #advertTtlMs;
32
+ #heartbeatMs;
33
+ #rateLimiter;
34
+ #nonceCache;
35
+ #now;
36
+ #actors = new Map();
37
+ #peers = new Map();
38
+ #inboxes = new Map();
39
+ #broadcastSubscribers = new Set();
40
+ #currentAdvert;
41
+ #advertSeq = 0;
42
+ constructor(config = {}) {
43
+ this.#logger = config.logger ?? CONSOLE_LOGGER;
44
+ this.#cors = config.cors ?? { mode: 'permissive' };
45
+ this.#clockSkewSec = config.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
46
+ this.#inboxBufferSize = config.inboxBufferSize ?? 100;
47
+ this.#advertTtlMs = config.advertTtlMs ?? DEFAULT_ADVERT_TTL_MS;
48
+ this.#heartbeatMs = config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
49
+ this.#rateLimiter = config.rateLimiter ?? new RateLimiter();
50
+ this.#nonceCache = config.nonceCache ?? new NonceCache();
51
+ this.#now = config.now ?? (() => Date.now());
52
+ }
53
+ // ----------------------------------------------------------------
54
+ // Transport interface
55
+ // ----------------------------------------------------------------
56
+ start() {
57
+ // No-op: server-side transport has no persistent outbound connections.
58
+ // SSE subscribers connect on demand via handleSse().
59
+ }
60
+ /**
61
+ * Detach the transport: close every open SSE subscription, clear the advert
62
+ * cache, and drop all actor / peer / inbox state. Intended for shutdown and
63
+ * for test teardown.
64
+ */
65
+ stop() {
66
+ for (const sub of this.#broadcastSubscribers)
67
+ this.#closeBroadcastSubscriber(sub);
68
+ this.#broadcastSubscribers.clear();
69
+ for (const inbox of this.#inboxes.values()) {
70
+ for (const sub of inbox.subscribers)
71
+ this.#closeInboxSubscriber(sub);
72
+ inbox.subscribers.clear();
73
+ }
74
+ this.#inboxes.clear();
75
+ this.#currentAdvert = undefined;
76
+ }
77
+ registerActor(did, keys) {
78
+ this.#actors.set(did, { keys, handlers: new Map() });
79
+ }
80
+ unregisterActor(did) {
81
+ this.#actors.delete(did);
82
+ this.#peers.delete(did);
83
+ }
84
+ getActorPk(did) {
85
+ return this.#actors.get(did)?.keys.publicKey.compressed;
86
+ }
87
+ registerPeer(did, communicationPk) {
88
+ try {
89
+ new CompressedSecp256k1PublicKey(communicationPk);
90
+ }
91
+ catch {
92
+ throw new HttpTransportError(`Invalid peer public key for ${did}`, 'INVALID_PEER_KEY', { did, keyLength: communicationPk.length });
93
+ }
94
+ this.#peers.set(did, communicationPk);
95
+ }
96
+ getPeerPk(did) {
97
+ return this.#peers.get(did);
98
+ }
99
+ registerMessageHandler(actorDid, messageType, handler) {
100
+ const actor = this.#actors.get(actorDid);
101
+ if (!actor) {
102
+ throw new HttpTransportError(`Cannot register handler: actor ${actorDid} not registered`, 'UNKNOWN_ACTOR', { did: actorDid });
103
+ }
104
+ actor.handlers.set(messageType, handler);
105
+ }
106
+ unregisterMessageHandler(actorDid, messageType) {
107
+ this.#actors.get(actorDid)?.handlers.delete(messageType);
108
+ }
109
+ async sendMessage(message, sender, recipient) {
110
+ if (!recipient) {
111
+ throw new HttpTransportError('HttpServerTransport.sendMessage requires a recipient. Use publishRepeating for broadcasts.', 'MISSING_RECIPIENT', { messageType: message.type });
112
+ }
113
+ const actor = this.#actors.get(sender);
114
+ if (!actor) {
115
+ throw new HttpTransportError(`Unknown sender: ${sender}`, 'UNKNOWN_SENDER', { did: sender });
116
+ }
117
+ const envelope = signEnvelope(message, { did: sender, keys: actor.keys }, { to: recipient });
118
+ const dataJson = JSON.stringify(envelope);
119
+ const inbox = this.#getOrCreateInbox(recipient);
120
+ const stored = inbox.buffer.append(SSE_EVENT.MESSAGE, dataJson);
121
+ for (const sub of inbox.subscribers) {
122
+ this.#safeWrite(sub.stream, stored.event, stored.data, stored.id);
123
+ }
124
+ }
125
+ publishRepeating(message, sender, _intervalMs, _recipient) {
126
+ const actor = this.#actors.get(sender);
127
+ if (!actor) {
128
+ throw new HttpTransportError(`Unknown sender: ${sender}`, 'UNKNOWN_SENDER', { did: sender });
129
+ }
130
+ const envelope = signEnvelope(message, { did: sender, keys: actor.keys });
131
+ const dataJson = JSON.stringify(envelope);
132
+ const id = String(++this.#advertSeq);
133
+ const expiresAtMs = this.#now() + this.#advertTtlMs;
134
+ this.#currentAdvert = { dataJson, id, expiresAtMs };
135
+ for (const sub of this.#broadcastSubscribers) {
136
+ this.#safeWrite(sub.stream, SSE_EVENT.ADVERT, dataJson, id);
137
+ }
138
+ return () => {
139
+ if (this.#currentAdvert?.id === id)
140
+ this.#currentAdvert = undefined;
141
+ };
142
+ }
143
+ // ----------------------------------------------------------------
144
+ // Sans-I/O HTTP surface
145
+ // ----------------------------------------------------------------
146
+ /**
147
+ * Handle a POST / GET request (non-SSE). The caller dispatches SSE paths to
148
+ * {@link handleSse} instead. Returns a fully formed response; the caller's
149
+ * adapter turns it into an HTTP write.
150
+ */
151
+ async handleRequest(req) {
152
+ const method = req.method.toUpperCase();
153
+ if (method === 'OPTIONS')
154
+ return this.#respond(204, '', req);
155
+ const path = extractPath(req.url);
156
+ if (method === 'GET' && path === HTTP_ROUTE.WELL_KNOWN) {
157
+ return this.#respondJson(200, this.#wellKnownMetadata(), req);
158
+ }
159
+ if (method === 'POST' && path === HTTP_ROUTE.MESSAGES) {
160
+ return await this.#handleMessagesPost(req);
161
+ }
162
+ if (method === 'POST' && path === HTTP_ROUTE.ADVERTS) {
163
+ return await this.#handleAdvertsPost(req);
164
+ }
165
+ return this.#respondJson(404, { error: 'not_found' }, req);
166
+ }
167
+ /**
168
+ * Open an SSE stream for a GET request. The caller is responsible for
169
+ * flushing writes and propagating the `onClose` callback when the HTTP
170
+ * connection ends.
171
+ */
172
+ handleSse(req, stream) {
173
+ if (req.method.toUpperCase() !== 'GET') {
174
+ stream.close();
175
+ return;
176
+ }
177
+ const path = extractPath(req.url);
178
+ if (path === HTTP_ROUTE.ADVERTS) {
179
+ this.#openBroadcastSubscription(stream);
180
+ return;
181
+ }
182
+ const inboxMatch = matchInboxPath(path);
183
+ if (inboxMatch) {
184
+ this.#openInboxSubscription(req, stream, inboxMatch.did, path);
185
+ return;
186
+ }
187
+ stream.close();
188
+ }
189
+ // ----------------------------------------------------------------
190
+ // Request handlers
191
+ // ----------------------------------------------------------------
192
+ async #handleMessagesPost(req) {
193
+ const envelope = parseJsonBody(req.body);
194
+ if (!envelope)
195
+ return this.#respondJson(400, { error: 'invalid_json' }, req);
196
+ const senderPk = this.#resolveSenderPk(envelope.from);
197
+ if (!senderPk) {
198
+ return this.#respondJson(401, { error: 'unknown_sender' }, req);
199
+ }
200
+ try {
201
+ verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
202
+ }
203
+ catch (err) {
204
+ this.#logger.debug('POST /v1/messages: envelope verification failed:', err);
205
+ return this.#respondJson(401, { error: 'invalid_envelope' }, req);
206
+ }
207
+ if (!this.#nonceCache.store(envelope.from, envelope.nonce, envelope.timestamp)) {
208
+ return this.#respondJson(409, { error: 'replay' }, req);
209
+ }
210
+ if (!this.#rateLimiter.consume(envelope.from, this.#now())) {
211
+ return this.#respondJson(429, { error: 'rate_limited' }, req);
212
+ }
213
+ if (!envelope.to) {
214
+ return this.#respondJson(400, { error: 'missing_recipient' }, req);
215
+ }
216
+ const actor = this.#actors.get(envelope.to);
217
+ if (!actor) {
218
+ return this.#respondJson(404, { error: 'unknown_recipient' }, req);
219
+ }
220
+ const revived = reviveFromWire(envelope.message);
221
+ const flat = flattenMessage(revived);
222
+ const messageType = typeof flat.type === 'string' ? flat.type : undefined;
223
+ if (!messageType)
224
+ return this.#respondJson(400, { error: 'missing_message_type' }, req);
225
+ const handler = actor.handlers.get(messageType);
226
+ if (handler) {
227
+ try {
228
+ await handler(flat);
229
+ }
230
+ catch (err) {
231
+ this.#logger.debug(`Handler threw for ${messageType}:`, err);
232
+ }
233
+ }
234
+ return this.#respondJson(202, { ok: true }, req);
235
+ }
236
+ async #handleAdvertsPost(req) {
237
+ const envelope = parseJsonBody(req.body);
238
+ if (!envelope)
239
+ return this.#respondJson(400, { error: 'invalid_json' }, req);
240
+ const senderPk = this.#resolveSenderPk(envelope.from);
241
+ if (!senderPk)
242
+ return this.#respondJson(401, { error: 'unknown_sender' }, req);
243
+ try {
244
+ verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
245
+ }
246
+ catch {
247
+ return this.#respondJson(401, { error: 'invalid_envelope' }, req);
248
+ }
249
+ if (!this.#nonceCache.store(envelope.from, envelope.nonce, envelope.timestamp)) {
250
+ return this.#respondJson(409, { error: 'replay' }, req);
251
+ }
252
+ if (!this.#rateLimiter.consume(envelope.from, this.#now())) {
253
+ return this.#respondJson(429, { error: 'rate_limited' }, req);
254
+ }
255
+ // Only registered actors can publish adverts on this server.
256
+ if (!this.#actors.has(envelope.from)) {
257
+ return this.#respondJson(403, { error: 'not_an_actor' }, req);
258
+ }
259
+ const id = String(++this.#advertSeq);
260
+ this.#currentAdvert = {
261
+ dataJson: JSON.stringify(envelope),
262
+ id,
263
+ expiresAtMs: this.#now() + this.#advertTtlMs,
264
+ };
265
+ for (const sub of this.#broadcastSubscribers) {
266
+ this.#safeWrite(sub.stream, SSE_EVENT.ADVERT, this.#currentAdvert.dataJson, id);
267
+ }
268
+ return this.#respondJson(202, { ok: true }, req);
269
+ }
270
+ #openBroadcastSubscription(stream) {
271
+ const sub = { stream };
272
+ this.#broadcastSubscribers.add(sub);
273
+ stream.onClose(() => {
274
+ this.#closeBroadcastSubscriber(sub);
275
+ this.#broadcastSubscribers.delete(sub);
276
+ });
277
+ // Replay current advert if still within TTL.
278
+ if (this.#currentAdvert && this.#currentAdvert.expiresAtMs > this.#now()) {
279
+ this.#safeWrite(stream, SSE_EVENT.ADVERT, this.#currentAdvert.dataJson, this.#currentAdvert.id);
280
+ }
281
+ if (this.#heartbeatMs > 0) {
282
+ sub.heartbeatTimer = setInterval(() => {
283
+ try {
284
+ stream.writeComment('hb');
285
+ }
286
+ catch { /* caller-owned failure */ }
287
+ }, this.#heartbeatMs);
288
+ }
289
+ }
290
+ #openInboxSubscription(req, stream, did, path) {
291
+ const auth = req.headers.authorization;
292
+ if (!auth) {
293
+ this.#logger.debug(`Inbox subscribe: missing authorization header for ${did}`);
294
+ stream.close();
295
+ return;
296
+ }
297
+ const senderPk = this.#resolveSenderPk(did);
298
+ if (!senderPk) {
299
+ stream.close();
300
+ return;
301
+ }
302
+ let parsedTs = 0;
303
+ let parsedNonce = '';
304
+ try {
305
+ const parsed = verifyRequestAuth(auth, path, senderPk, {
306
+ clockSkewSec: this.#clockSkewSec,
307
+ now: () => this.#now(),
308
+ });
309
+ if (parsed.did !== did) {
310
+ stream.close();
311
+ return;
312
+ }
313
+ parsedTs = parsed.ts;
314
+ parsedNonce = parsed.nonce;
315
+ }
316
+ catch (err) {
317
+ this.#logger.debug(`Inbox subscribe: auth verification failed for ${did}:`, err);
318
+ stream.close();
319
+ return;
320
+ }
321
+ if (!this.#nonceCache.store(did, parsedNonce, parsedTs)) {
322
+ stream.close();
323
+ return;
324
+ }
325
+ if (!this.#rateLimiter.consume(did, this.#now())) {
326
+ stream.close();
327
+ return;
328
+ }
329
+ const inbox = this.#getOrCreateInbox(did);
330
+ const sub = { stream };
331
+ inbox.subscribers.add(sub);
332
+ stream.onClose(() => {
333
+ this.#closeInboxSubscriber(sub);
334
+ inbox.subscribers.delete(sub);
335
+ });
336
+ // Replay buffered events since the client's Last-Event-ID, if any.
337
+ const lastEventId = req.headers['last-event-id'];
338
+ for (const stored of inbox.buffer.since(lastEventId)) {
339
+ this.#safeWrite(stream, stored.event, stored.data, stored.id);
340
+ }
341
+ if (this.#heartbeatMs > 0) {
342
+ sub.heartbeatTimer = setInterval(() => {
343
+ try {
344
+ stream.writeComment('hb');
345
+ }
346
+ catch { /* caller-owned failure */ }
347
+ }, this.#heartbeatMs);
348
+ }
349
+ }
350
+ // ----------------------------------------------------------------
351
+ // Internal helpers
352
+ // ----------------------------------------------------------------
353
+ #getOrCreateInbox(did) {
354
+ let inbox = this.#inboxes.get(did);
355
+ if (!inbox) {
356
+ inbox = { buffer: new InboxBuffer(this.#inboxBufferSize), subscribers: new Set() };
357
+ this.#inboxes.set(did, inbox);
358
+ }
359
+ return inbox;
360
+ }
361
+ #resolveSenderPk(did) {
362
+ const peerBytes = this.#peers.get(did);
363
+ if (peerBytes) {
364
+ try {
365
+ return new CompressedSecp256k1PublicKey(peerBytes);
366
+ }
367
+ catch { /* fall through */ }
368
+ }
369
+ try {
370
+ const components = Identifier.decode(did);
371
+ if (components.idType === 'KEY') {
372
+ return new CompressedSecp256k1PublicKey(components.genesisBytes);
373
+ }
374
+ }
375
+ catch { /* not decodable */ }
376
+ return undefined;
377
+ }
378
+ #safeWrite(stream, event, data, id) {
379
+ try {
380
+ stream.writeEvent(event, data, id);
381
+ }
382
+ catch (err) {
383
+ this.#logger.debug('SSE writeEvent failed:', err);
384
+ }
385
+ }
386
+ #closeBroadcastSubscriber(sub) {
387
+ if (sub.heartbeatTimer)
388
+ clearInterval(sub.heartbeatTimer);
389
+ try {
390
+ sub.stream.close();
391
+ }
392
+ catch { /* already closed */ }
393
+ }
394
+ #closeInboxSubscriber(sub) {
395
+ if (sub.heartbeatTimer)
396
+ clearInterval(sub.heartbeatTimer);
397
+ try {
398
+ sub.stream.close();
399
+ }
400
+ catch { /* already closed */ }
401
+ }
402
+ #respondJson(status, body, req) {
403
+ return {
404
+ status,
405
+ headers: { 'content-type': 'application/json', ...this.#corsHeaders(req) },
406
+ body: JSON.stringify(body),
407
+ };
408
+ }
409
+ #respond(status, body, req) {
410
+ return { status, headers: this.#corsHeaders(req), body };
411
+ }
412
+ #corsHeaders(req) {
413
+ const origin = req.headers.origin;
414
+ if (!origin)
415
+ return {};
416
+ const common = {
417
+ 'access-control-allow-methods': 'GET, POST, OPTIONS',
418
+ 'access-control-allow-headers': 'authorization, content-type, last-event-id',
419
+ 'access-control-max-age': '86400',
420
+ };
421
+ switch (this.#cors.mode) {
422
+ case 'permissive':
423
+ return { 'access-control-allow-origin': '*', ...common };
424
+ case 'allowlist':
425
+ if (this.#cors.origins.includes(origin)) {
426
+ return { 'access-control-allow-origin': origin, vary: 'origin', ...common };
427
+ }
428
+ return {};
429
+ case 'same-origin':
430
+ return {};
431
+ }
432
+ }
433
+ #wellKnownMetadata() {
434
+ return {
435
+ envelopeVersion: HTTP_ENVELOPE_VERSION,
436
+ heartbeatIntervalMs: this.#heartbeatMs,
437
+ inboxBufferSize: this.#inboxBufferSize,
438
+ advertTtlMs: this.#advertTtlMs,
439
+ };
440
+ }
441
+ }
442
+ // ----------------------------------------------------------------
443
+ // Module-local pure helpers
444
+ // ----------------------------------------------------------------
445
+ function extractPath(reqUrl) {
446
+ if (reqUrl.startsWith('http://') || reqUrl.startsWith('https://')) {
447
+ return new URL(reqUrl).pathname;
448
+ }
449
+ const q = reqUrl.indexOf('?');
450
+ return q === -1 ? reqUrl : reqUrl.slice(0, q);
451
+ }
452
+ function matchInboxPath(path) {
453
+ if (!path.startsWith(INBOX_PATH_PREFIX) || !path.endsWith(INBOX_PATH_SUFFIX))
454
+ return undefined;
455
+ const encodedDid = path.slice(INBOX_PATH_PREFIX.length, path.length - INBOX_PATH_SUFFIX.length);
456
+ if (!encodedDid)
457
+ return undefined;
458
+ try {
459
+ return { did: decodeURIComponent(encodedDid) };
460
+ }
461
+ catch {
462
+ return undefined;
463
+ }
464
+ }
465
+ function parseJsonBody(body) {
466
+ if (body === undefined || body === '')
467
+ return undefined;
468
+ try {
469
+ return JSON.parse(body);
470
+ }
471
+ catch {
472
+ return undefined;
473
+ }
474
+ }
475
+ function flattenMessage(msg) {
476
+ if (msg.body && typeof msg.body === 'object') {
477
+ return { ...msg, ...msg.body };
478
+ }
479
+ return msg;
480
+ }
481
+ //# sourceMappingURL=server.js.map