@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,43 @@
1
+ export interface NonceCacheConfig {
2
+ /** Max distinct entries to retain before FIFO eviction. Default 10,000. */
3
+ maxEntries?: number;
4
+ }
5
+
6
+ /**
7
+ * Bounded anti-replay cache for `(did, nonce)` pairs.
8
+ *
9
+ * Replay windowing is the caller's responsibility — this cache only detects
10
+ * duplicates. Callers are expected to reject envelopes/headers whose timestamp
11
+ * is outside the clock-skew window *before* consulting the cache, so entries
12
+ * here are always within the protocol's acceptable window.
13
+ *
14
+ * Eviction is strict-FIFO (Map insertion order) once `maxEntries` is reached.
15
+ */
16
+ export class NonceCache {
17
+ readonly #maxEntries: number;
18
+ readonly #entries = new Map<string, number>();
19
+
20
+ constructor(config: NonceCacheConfig = {}) {
21
+ this.#maxEntries = config.maxEntries ?? 10_000;
22
+ }
23
+
24
+ /**
25
+ * Record a nonce. Returns `true` if it was novel (caller should accept the
26
+ * request) or `false` if it was a replay (caller should reject).
27
+ */
28
+ store(did: string, nonce: string, timestampSec: number): boolean {
29
+ const key = `${did}:${nonce}`;
30
+ if(this.#entries.has(key)) return false;
31
+ this.#entries.set(key, timestampSec);
32
+ if(this.#entries.size > this.#maxEntries) {
33
+ const oldest = this.#entries.keys().next();
34
+ if(!oldest.done) this.#entries.delete(oldest.value);
35
+ }
36
+ return true;
37
+ }
38
+
39
+ /** Current cache size. Exposed for observability and tests. */
40
+ size(): number {
41
+ return this.#entries.size;
42
+ }
43
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * On-the-wire version of the HTTP transport envelope. Separate from
3
+ * {@link AGGREGATION_WIRE_VERSION} (which versions the aggregation protocol
4
+ * payloads) so transport-layer changes don't force a protocol bump.
5
+ */
6
+ export const HTTP_ENVELOPE_VERSION = 1;
7
+
8
+ /**
9
+ * HTTP routes served by {@link HttpServerTransport}. The `{did}` placeholder
10
+ * in {@link HTTP_ROUTE.ACTOR_INBOX} is substituted with a URL-safe DID at
11
+ * request time.
12
+ */
13
+ export const HTTP_ROUTE = {
14
+ ADVERTS : '/v1/adverts',
15
+ MESSAGES : '/v1/messages',
16
+ ACTOR_INBOX : '/v1/actors/{did}/inbox',
17
+ WELL_KNOWN : '/v1/.well-known/aggregation',
18
+ } as const;
19
+
20
+ /** Server-Sent Events event names used by the broadcast + inbox streams. */
21
+ export const SSE_EVENT = {
22
+ ADVERT : 'advert',
23
+ MESSAGE : 'message',
24
+ HEARTBEAT : 'heartbeat',
25
+ } as const;
26
+
27
+ /** Default clock-skew tolerance for envelope timestamps (seconds). */
28
+ export const DEFAULT_CLOCK_SKEW_SEC = 60;
29
+
30
+ /** Default length of the per-envelope anti-replay nonce (bytes). */
31
+ export const DEFAULT_NONCE_LEN_BYTES = 16;
32
+
33
+ /**
34
+ * Tamper-evident wrapper around an aggregation {@link BaseMessage}. Every
35
+ * authenticated HTTP request and inbox SSE event carries one of these.
36
+ *
37
+ * The signature is BIP340 Schnorr over the SHA-256 of the JCS-canonicalized
38
+ * envelope **excluding** the `sig` field. Receivers reconstruct the same
39
+ * canonical form, verify the signature against the sender's registered
40
+ * communication public key, and reject outside-skew timestamps.
41
+ */
42
+ export interface SignedEnvelope {
43
+ /** Envelope format version; must equal {@link HTTP_ENVELOPE_VERSION}. */
44
+ v: number;
45
+ /** Sender DID. */
46
+ from: string;
47
+ /** Recipient DID. Omitted for broadcasts (e.g. COHORT_ADVERT). */
48
+ to?: string;
49
+ /** Unix seconds at which the envelope was produced. */
50
+ timestamp: number;
51
+ /** Hex-encoded random nonce, {@link DEFAULT_NONCE_LEN_BYTES} bytes. */
52
+ nonce: string;
53
+ /** Aggregation message payload (plain-JSON form, `toJSON`-normalized). */
54
+ message: Record<string, unknown>;
55
+ /** Hex-encoded 64-byte BIP340 Schnorr signature. */
56
+ sig: string;
57
+ }
@@ -0,0 +1,75 @@
1
+ export interface BucketState {
2
+ tokens: number;
3
+ lastRefillMs: number;
4
+ }
5
+
6
+ /**
7
+ * Pluggable storage backend for rate-limit state. The default
8
+ * {@link InMemoryRateLimitStore} is a plain Map; swap in Redis-backed or
9
+ * SQLite-backed implementations for multi-instance deployments.
10
+ */
11
+ export interface RateLimitStore {
12
+ get(key: string): BucketState | undefined;
13
+ set(key: string, state: BucketState): void;
14
+ }
15
+
16
+ export class InMemoryRateLimitStore implements RateLimitStore {
17
+ readonly #buckets = new Map<string, BucketState>();
18
+
19
+ get(key: string): BucketState | undefined {
20
+ return this.#buckets.get(key);
21
+ }
22
+
23
+ set(key: string, state: BucketState): void {
24
+ this.#buckets.set(key, state);
25
+ }
26
+ }
27
+
28
+ export interface RateLimiterConfig {
29
+ /** Steady-state request rate per key (requests per second). Default 10. */
30
+ rps?: number;
31
+ /** Peak burst allowance (bucket capacity). Default 30. */
32
+ burst?: number;
33
+ /** Optional pluggable store. Defaults to an in-memory Map. */
34
+ store?: RateLimitStore;
35
+ }
36
+
37
+ /**
38
+ * Token-bucket rate limiter keyed by an opaque string (typically a verified
39
+ * sender DID). Tokens refill linearly at `rps` up to `burst`. Each `consume`
40
+ * call atomically debits one token or returns `false` to reject.
41
+ *
42
+ * The limiter is synchronous and deterministic given `nowMs` — tests can
43
+ * drive it with a fixed clock to exercise exact boundaries.
44
+ */
45
+ export class RateLimiter {
46
+ readonly #rps: number;
47
+ readonly #burst: number;
48
+ readonly #store: RateLimitStore;
49
+
50
+ constructor(config: RateLimiterConfig = {}) {
51
+ this.#rps = config.rps ?? 10;
52
+ this.#burst = config.burst ?? 30;
53
+ this.#store = config.store ?? new InMemoryRateLimitStore();
54
+ }
55
+
56
+ /** Consume one token for `key`. Returns `true` if accepted, `false` if throttled. */
57
+ consume(key: string, nowMs: number): boolean {
58
+ const existing = this.#store.get(key);
59
+ const state: BucketState = existing ?? { tokens: this.#burst, lastRefillMs: nowMs };
60
+
61
+ if(existing) {
62
+ const elapsedSec = Math.max(0, (nowMs - existing.lastRefillMs) / 1000);
63
+ state.tokens = Math.min(this.#burst, existing.tokens + elapsedSec * this.#rps);
64
+ state.lastRefillMs = nowMs;
65
+ }
66
+
67
+ if(state.tokens < 1) {
68
+ this.#store.set(key, state);
69
+ return false;
70
+ }
71
+ state.tokens -= 1;
72
+ this.#store.set(key, state);
73
+ return true;
74
+ }
75
+ }
@@ -0,0 +1,164 @@
1
+ import { canonicalHashBytes } from '@did-btcr2/common';
2
+ import type { CompressedSecp256k1PublicKey, SchnorrKeyPair } from '@did-btcr2/keypair';
3
+ import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils';
4
+
5
+ import { HttpTransportError } from './errors.js';
6
+ import { DEFAULT_CLOCK_SKEW_SEC, DEFAULT_NONCE_LEN_BYTES, HTTP_ENVELOPE_VERSION } from './protocol.js';
7
+
8
+ /**
9
+ * `Authorization`-header scheme used to authenticate SSE subscription
10
+ * requests. The header value takes the form
11
+ * `BTCR2-Sig v=<n>,did=<did>,ts=<unix>,nonce=<hex>,sig=<hex>`.
12
+ *
13
+ * Used only for GET endpoints (SSE inbox subscribe). POST endpoints carry a
14
+ * full {@link SignedEnvelope} in the request body instead.
15
+ */
16
+ export const REQUEST_AUTH_SCHEME = 'BTCR2-Sig';
17
+
18
+ export interface ParsedRequestAuth {
19
+ /** Transport envelope format version. */
20
+ v: number;
21
+ /** Subscriber DID. */
22
+ did: string;
23
+ /** Unix-seconds timestamp. */
24
+ ts: number;
25
+ /** Hex-encoded anti-replay nonce. */
26
+ nonce: string;
27
+ /** Hex-encoded 64-byte BIP340 signature. */
28
+ sig: string;
29
+ }
30
+
31
+ export interface BuildRequestAuthOptions {
32
+ nonce?: string;
33
+ timestamp?: number;
34
+ }
35
+
36
+ /**
37
+ * Build an `Authorization` header value proving the caller controls the
38
+ * private key for `did`. Covers a specific request path so the signature
39
+ * can't be replayed against a different endpoint.
40
+ */
41
+ export function buildRequestAuth(
42
+ did: string,
43
+ keys: SchnorrKeyPair,
44
+ path: string,
45
+ opts: BuildRequestAuthOptions = {},
46
+ ): string {
47
+ const ts = opts.timestamp ?? Math.floor(Date.now() / 1000);
48
+ const nonce = opts.nonce ?? bytesToHex(randomBytes(DEFAULT_NONCE_LEN_BYTES));
49
+
50
+ const digest = canonicalHashBytes({
51
+ v : HTTP_ENVELOPE_VERSION,
52
+ did,
53
+ ts,
54
+ nonce,
55
+ path,
56
+ });
57
+ const sig = keys.secretKey.sign(digest, { scheme: 'schnorr' });
58
+
59
+ return `${REQUEST_AUTH_SCHEME} v=${HTTP_ENVELOPE_VERSION},did=${did},ts=${ts},nonce=${nonce},sig=${bytesToHex(sig)}`;
60
+ }
61
+
62
+ /**
63
+ * Parse a `BTCR2-Sig` auth header value into its structured fields. Does NOT
64
+ * verify the signature — call {@link verifyRequestAuth} for that.
65
+ */
66
+ export function parseRequestAuth(headerValue: string): ParsedRequestAuth {
67
+ const prefix = `${REQUEST_AUTH_SCHEME} `;
68
+ if(!headerValue.startsWith(prefix)) {
69
+ throw new HttpTransportError(
70
+ `Unexpected auth scheme (want ${REQUEST_AUTH_SCHEME})`,
71
+ 'REQUEST_AUTH_SCHEME',
72
+ );
73
+ }
74
+
75
+ const params: Record<string, string> = {};
76
+ for(const piece of headerValue.slice(prefix.length).split(',')) {
77
+ const eq = piece.indexOf('=');
78
+ if(eq === -1) continue;
79
+ const key = piece.slice(0, eq).trim();
80
+ const val = piece.slice(eq + 1).trim();
81
+ if(key.length > 0) params[key] = val;
82
+ }
83
+
84
+ const v = Number(params.v);
85
+ const ts = Number(params.ts);
86
+ if(!Number.isInteger(v) || !Number.isInteger(ts) || !params.did || !params.nonce || !params.sig) {
87
+ throw new HttpTransportError(
88
+ 'Malformed auth header (missing or invalid field)',
89
+ 'REQUEST_AUTH_MALFORMED',
90
+ { received: Object.keys(params) },
91
+ );
92
+ }
93
+ return { v, did: params.did, ts, nonce: params.nonce, sig: params.sig };
94
+ }
95
+
96
+ export interface VerifyRequestAuthOptions {
97
+ clockSkewSec?: number;
98
+ now?: () => number;
99
+ }
100
+
101
+ /**
102
+ * Parse + verify an auth header. Throws {@link HttpTransportError} on any
103
+ * failure; returns the parsed fields on success.
104
+ *
105
+ * `expectedPath` must match the path the signature commits to. `senderPk`
106
+ * must correspond to the DID the signer claims.
107
+ */
108
+ export function verifyRequestAuth(
109
+ headerValue: string,
110
+ expectedPath: string,
111
+ senderPk: CompressedSecp256k1PublicKey,
112
+ opts: VerifyRequestAuthOptions = {},
113
+ ): ParsedRequestAuth {
114
+ const parsed = parseRequestAuth(headerValue);
115
+
116
+ if(parsed.v !== HTTP_ENVELOPE_VERSION) {
117
+ throw new HttpTransportError(
118
+ `Unsupported auth version: ${parsed.v}`,
119
+ 'REQUEST_AUTH_VERSION_MISMATCH',
120
+ { version: parsed.v, expected: HTTP_ENVELOPE_VERSION },
121
+ );
122
+ }
123
+
124
+ const skewSec = opts.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
125
+ const nowMs = opts.now ? opts.now() : Date.now();
126
+ const nowSec = Math.floor(nowMs / 1000);
127
+ const diff = Math.abs(nowSec - parsed.ts);
128
+ if(diff > skewSec) {
129
+ throw new HttpTransportError(
130
+ `Auth timestamp out of skew: ${diff}s > ${skewSec}s`,
131
+ 'REQUEST_AUTH_TIMESTAMP_SKEW',
132
+ { diff, skewSec },
133
+ );
134
+ }
135
+
136
+ let sigBytes: Uint8Array;
137
+ try {
138
+ sigBytes = hexToBytes(parsed.sig);
139
+ } catch {
140
+ throw new HttpTransportError('Auth signature is not valid hex', 'REQUEST_AUTH_SIG_HEX');
141
+ }
142
+ if(sigBytes.length !== 64) {
143
+ throw new HttpTransportError(
144
+ `Invalid auth signature length: ${sigBytes.length}`,
145
+ 'REQUEST_AUTH_SIG_LENGTH',
146
+ { length: sigBytes.length },
147
+ );
148
+ }
149
+
150
+ const digest = canonicalHashBytes({
151
+ v : parsed.v,
152
+ did : parsed.did,
153
+ ts : parsed.ts,
154
+ nonce : parsed.nonce,
155
+ path : expectedPath,
156
+ });
157
+
158
+ const ok = senderPk.verify(sigBytes, digest, { scheme: 'schnorr' });
159
+ if(!ok) {
160
+ throw new HttpTransportError('Auth signature verification failed', 'REQUEST_AUTH_SIG_INVALID');
161
+ }
162
+
163
+ return parsed;
164
+ }