@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.
- package/README.md +13 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +34125 -44647
- package/dist/browser.mjs +26409 -36931
- package/dist/cjs/index.js +2869 -679
- package/dist/esm/core/aggregation/beacon-strategy.js +62 -0
- package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -0
- package/dist/esm/core/aggregation/cohort.js +31 -8
- package/dist/esm/core/aggregation/cohort.js.map +1 -1
- package/dist/esm/core/aggregation/logger.js +15 -0
- package/dist/esm/core/aggregation/logger.js.map +1 -0
- package/dist/esm/core/aggregation/messages/base.js +12 -1
- package/dist/esm/core/aggregation/messages/base.js.map +1 -1
- package/dist/esm/core/aggregation/messages/bodies.js +90 -0
- package/dist/esm/core/aggregation/messages/bodies.js.map +1 -0
- package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
- package/dist/esm/core/aggregation/messages/index.js +1 -0
- package/dist/esm/core/aggregation/messages/index.js.map +1 -1
- package/dist/esm/core/aggregation/participant.js +39 -46
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +33 -7
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +198 -19
- package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
- package/dist/esm/core/aggregation/service.js +143 -15
- package/dist/esm/core/aggregation/service.js.map +1 -1
- package/dist/esm/core/aggregation/signing-session.js +44 -5
- package/dist/esm/core/aggregation/signing-session.js.map +1 -1
- package/dist/esm/core/aggregation/transport/didcomm.js +9 -0
- package/dist/esm/core/aggregation/transport/didcomm.js.map +1 -1
- 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/aggregation/transport/nostr.js +245 -16
- package/dist/esm/core/aggregation/transport/nostr.js.map +1 -1
- package/dist/esm/core/beacon/beacon.js +295 -63
- 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/beacon/utils.js +14 -9
- package/dist/esm/core/beacon/utils.js.map +1 -1
- package/dist/esm/core/updater.js +63 -55
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/esm/did-btcr2.js +0 -4
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/did-document.js +2 -2
- package/dist/esm/utils/did-document.js.map +1 -1
- package/dist/types/core/aggregation/beacon-strategy.d.ts +52 -0
- package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -0
- package/dist/types/core/aggregation/cohort.d.ts +20 -3
- package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
- package/dist/types/core/aggregation/logger.d.ts +22 -0
- package/dist/types/core/aggregation/logger.d.ts.map +1 -0
- package/dist/types/core/aggregation/messages/base.d.ts +13 -1
- package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/bodies.d.ts +130 -0
- package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -0
- package/dist/types/core/aggregation/messages/factories.d.ts +1 -0
- package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/index.d.ts +1 -0
- package/dist/types/core/aggregation/messages/index.d.ts.map +1 -1
- package/dist/types/core/aggregation/participant.d.ts +2 -0
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +32 -6
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +7 -5
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +33 -3
- package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/service.d.ts +33 -2
- package/dist/types/core/aggregation/service.d.ts.map +1 -1
- package/dist/types/core/aggregation/signing-session.d.ts +5 -1
- package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/didcomm.d.ts +3 -0
- package/dist/types/core/aggregation/transport/didcomm.d.ts.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/nostr.d.ts +99 -1
- package/dist/types/core/aggregation/transport/nostr.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/transport.d.ts +26 -1
- package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +149 -22
- 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/beacon/utils.d.ts +2 -2
- package/dist/types/core/beacon/utils.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/dist/types/did-btcr2.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +5 -7
- package/src/core/aggregation/beacon-strategy.ts +123 -0
- package/src/core/aggregation/cohort.ts +34 -8
- package/src/core/aggregation/logger.ts +33 -0
- package/src/core/aggregation/messages/base.ts +20 -5
- package/src/core/aggregation/messages/bodies.ts +223 -0
- package/src/core/aggregation/messages/factories.ts +1 -0
- package/src/core/aggregation/messages/index.ts +1 -0
- package/src/core/aggregation/participant.ts +40 -46
- package/src/core/aggregation/runner/events.ts +27 -3
- package/src/core/aggregation/runner/participant-runner.ts +41 -7
- package/src/core/aggregation/runner/service-runner.ts +227 -19
- package/src/core/aggregation/service.ts +189 -20
- package/src/core/aggregation/signing-session.ts +65 -7
- package/src/core/aggregation/transport/didcomm.ts +17 -0
- 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/nostr.ts +266 -23
- package/src/core/aggregation/transport/transport.ts +34 -1
- package/src/core/beacon/beacon.ts +411 -79
- 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/beacon/utils.ts +16 -11
- package/src/core/updater.ts +113 -67
- package/src/did-btcr2.ts +0 -5
- package/src/index.ts +2 -0
- package/src/utils/did-document.ts +2 -2
|
@@ -0,0 +1,615 @@
|
|
|
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 { InboxBuffer } from './inbox-buffer.js';
|
|
12
|
+
import { NonceCache } from './nonce-cache.js';
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_CLOCK_SKEW_SEC,
|
|
15
|
+
HTTP_ENVELOPE_VERSION,
|
|
16
|
+
HTTP_ROUTE,
|
|
17
|
+
SSE_EVENT,
|
|
18
|
+
type SignedEnvelope,
|
|
19
|
+
} from './protocol.js';
|
|
20
|
+
import { RateLimiter } from './rate-limiter.js';
|
|
21
|
+
import { verifyRequestAuth } from './request-auth.js';
|
|
22
|
+
|
|
23
|
+
/** Framework-agnostic incoming-request shape. */
|
|
24
|
+
export interface HttpRequestLike {
|
|
25
|
+
method: string;
|
|
26
|
+
/** Either a full URL or path+query; path extraction works with both. */
|
|
27
|
+
url: string;
|
|
28
|
+
/** Header names MUST be lowercased. */
|
|
29
|
+
headers: Record<string, string>;
|
|
30
|
+
/** Request body (already read). Undefined for GETs. */
|
|
31
|
+
body?: string;
|
|
32
|
+
/** Optional remote-address hint for per-IP policies (advert rate limits etc). */
|
|
33
|
+
remoteAddr?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Framework-agnostic outgoing-response shape. */
|
|
37
|
+
export interface HttpResponseLike {
|
|
38
|
+
status: number;
|
|
39
|
+
headers: Record<string, string>;
|
|
40
|
+
body: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Framework-agnostic SSE stream handle. Callers (Express/Hono/Fastify/Workers)
|
|
45
|
+
* adapt this to their response-writing primitives. All methods are synchronous
|
|
46
|
+
* from the transport's perspective; the adapter is free to buffer/batch.
|
|
47
|
+
*/
|
|
48
|
+
export interface SseStream {
|
|
49
|
+
/** Write a named event with data and optional id. */
|
|
50
|
+
writeEvent(event: string, data: string, id?: string): void;
|
|
51
|
+
/** Write a comment frame (keepalive; ignored by parsers). */
|
|
52
|
+
writeComment(comment: string): void;
|
|
53
|
+
/** Close the stream from the server side. */
|
|
54
|
+
close(): void;
|
|
55
|
+
/** Register a callback fired when the client disconnects (or we close). */
|
|
56
|
+
onClose(cb: () => void): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type CorsPolicy =
|
|
60
|
+
| { mode: 'permissive' }
|
|
61
|
+
| { mode: 'allowlist'; origins: string[] }
|
|
62
|
+
| { mode: 'same-origin' };
|
|
63
|
+
|
|
64
|
+
export interface HttpServerTransportConfig {
|
|
65
|
+
logger?: Logger;
|
|
66
|
+
/** CORS policy. Defaults to `{ mode: 'permissive' }`. */
|
|
67
|
+
cors?: CorsPolicy;
|
|
68
|
+
/** Envelope / request-auth clock-skew tolerance, seconds. */
|
|
69
|
+
clockSkewSec?: number;
|
|
70
|
+
/** Per-recipient inbox buffer size. Default 100. */
|
|
71
|
+
inboxBufferSize?: number;
|
|
72
|
+
/** Advert cache TTL, milliseconds. Default 5 minutes. */
|
|
73
|
+
advertTtlMs?: number;
|
|
74
|
+
/** Custom rate limiter (pre-configured). If absent, uses defaults. */
|
|
75
|
+
rateLimiter?: RateLimiter;
|
|
76
|
+
/** Custom nonce cache (pre-configured). If absent, uses defaults. */
|
|
77
|
+
nonceCache?: NonceCache;
|
|
78
|
+
/** SSE heartbeat interval, milliseconds. Default 20000. Set 0 to disable. */
|
|
79
|
+
heartbeatIntervalMs?: number;
|
|
80
|
+
/** Clock injection point for tests. Returns unix milliseconds. */
|
|
81
|
+
now?: () => number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ActorEntry {
|
|
85
|
+
keys: SchnorrKeyPair;
|
|
86
|
+
handlers: Map<string, MessageHandler>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface InboxSubscriber {
|
|
90
|
+
stream: SseStream;
|
|
91
|
+
heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface BroadcastSubscriber {
|
|
95
|
+
stream: SseStream;
|
|
96
|
+
heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface InboxState {
|
|
100
|
+
buffer: InboxBuffer;
|
|
101
|
+
subscribers: Set<InboxSubscriber>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface CurrentAdvert {
|
|
105
|
+
dataJson: string;
|
|
106
|
+
id: string;
|
|
107
|
+
expiresAtMs: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const INBOX_PATH_PREFIX = '/v1/actors/';
|
|
111
|
+
const INBOX_PATH_SUFFIX = '/inbox';
|
|
112
|
+
|
|
113
|
+
const DEFAULT_ADVERT_TTL_MS = 5 * 60 * 1000;
|
|
114
|
+
const DEFAULT_HEARTBEAT_MS = 20_000;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Server-side HTTP transport. Sans-I/O — the caller mounts
|
|
118
|
+
* {@link handleRequest} and {@link handleSse} under their HTTP framework of
|
|
119
|
+
* choice; the transport owns only in-memory state (actors, inboxes, advert
|
|
120
|
+
* cache, replay / rate-limit policies).
|
|
121
|
+
*
|
|
122
|
+
* Implements the generic {@link Transport} interface so the aggregation
|
|
123
|
+
* runners can drive it exactly the same way they drive {@link NostrTransport}
|
|
124
|
+
* or {@link HttpClientTransport}.
|
|
125
|
+
*/
|
|
126
|
+
export class HttpServerTransport implements Transport {
|
|
127
|
+
readonly name = 'http';
|
|
128
|
+
|
|
129
|
+
readonly #logger: Logger;
|
|
130
|
+
readonly #cors: CorsPolicy;
|
|
131
|
+
readonly #clockSkewSec: number;
|
|
132
|
+
readonly #inboxBufferSize: number;
|
|
133
|
+
readonly #advertTtlMs: number;
|
|
134
|
+
readonly #heartbeatMs: number;
|
|
135
|
+
readonly #rateLimiter: RateLimiter;
|
|
136
|
+
readonly #nonceCache: NonceCache;
|
|
137
|
+
readonly #now: () => number;
|
|
138
|
+
|
|
139
|
+
readonly #actors: Map<string, ActorEntry> = new Map();
|
|
140
|
+
readonly #peers: Map<string, Uint8Array> = new Map();
|
|
141
|
+
readonly #inboxes: Map<string, InboxState> = new Map();
|
|
142
|
+
|
|
143
|
+
readonly #broadcastSubscribers: Set<BroadcastSubscriber> = new Set();
|
|
144
|
+
|
|
145
|
+
#currentAdvert?: CurrentAdvert;
|
|
146
|
+
#advertSeq = 0;
|
|
147
|
+
|
|
148
|
+
constructor(config: HttpServerTransportConfig = {}) {
|
|
149
|
+
this.#logger = config.logger ?? CONSOLE_LOGGER;
|
|
150
|
+
this.#cors = config.cors ?? { mode: 'permissive' };
|
|
151
|
+
this.#clockSkewSec = config.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
|
|
152
|
+
this.#inboxBufferSize = config.inboxBufferSize ?? 100;
|
|
153
|
+
this.#advertTtlMs = config.advertTtlMs ?? DEFAULT_ADVERT_TTL_MS;
|
|
154
|
+
this.#heartbeatMs = config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
|
|
155
|
+
this.#rateLimiter = config.rateLimiter ?? new RateLimiter();
|
|
156
|
+
this.#nonceCache = config.nonceCache ?? new NonceCache();
|
|
157
|
+
this.#now = config.now ?? (() => Date.now());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ----------------------------------------------------------------
|
|
161
|
+
// Transport interface
|
|
162
|
+
// ----------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
start(): void {
|
|
165
|
+
// No-op: server-side transport has no persistent outbound connections.
|
|
166
|
+
// SSE subscribers connect on demand via handleSse().
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Detach the transport: close every open SSE subscription, clear the advert
|
|
171
|
+
* cache, and drop all actor / peer / inbox state. Intended for shutdown and
|
|
172
|
+
* for test teardown.
|
|
173
|
+
*/
|
|
174
|
+
stop(): void {
|
|
175
|
+
for(const sub of this.#broadcastSubscribers) this.#closeBroadcastSubscriber(sub);
|
|
176
|
+
this.#broadcastSubscribers.clear();
|
|
177
|
+
for(const inbox of this.#inboxes.values()) {
|
|
178
|
+
for(const sub of inbox.subscribers) this.#closeInboxSubscriber(sub);
|
|
179
|
+
inbox.subscribers.clear();
|
|
180
|
+
}
|
|
181
|
+
this.#inboxes.clear();
|
|
182
|
+
this.#currentAdvert = undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
registerActor(did: string, keys: SchnorrKeyPair): void {
|
|
186
|
+
this.#actors.set(did, { keys, handlers: new Map() });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
unregisterActor(did: string): void {
|
|
190
|
+
this.#actors.delete(did);
|
|
191
|
+
this.#peers.delete(did);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getActorPk(did: string): Uint8Array | undefined {
|
|
195
|
+
return this.#actors.get(did)?.keys.publicKey.compressed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
registerPeer(did: string, communicationPk: Uint8Array): void {
|
|
199
|
+
try {
|
|
200
|
+
new CompressedSecp256k1PublicKey(communicationPk);
|
|
201
|
+
} catch {
|
|
202
|
+
throw new HttpTransportError(
|
|
203
|
+
`Invalid peer public key for ${did}`,
|
|
204
|
+
'INVALID_PEER_KEY',
|
|
205
|
+
{ did, keyLength: communicationPk.length },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
this.#peers.set(did, communicationPk);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getPeerPk(did: string): Uint8Array | undefined {
|
|
212
|
+
return this.#peers.get(did);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
registerMessageHandler(actorDid: string, messageType: string, handler: MessageHandler): void {
|
|
216
|
+
const actor = this.#actors.get(actorDid);
|
|
217
|
+
if(!actor) {
|
|
218
|
+
throw new HttpTransportError(
|
|
219
|
+
`Cannot register handler: actor ${actorDid} not registered`,
|
|
220
|
+
'UNKNOWN_ACTOR',
|
|
221
|
+
{ did: actorDid },
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
actor.handlers.set(messageType, handler);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
unregisterMessageHandler(actorDid: string, messageType: string): void {
|
|
228
|
+
this.#actors.get(actorDid)?.handlers.delete(messageType);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async sendMessage(message: BaseMessage, sender: string, recipient?: string): Promise<void> {
|
|
232
|
+
if(!recipient) {
|
|
233
|
+
throw new HttpTransportError(
|
|
234
|
+
'HttpServerTransport.sendMessage requires a recipient. Use publishRepeating for broadcasts.',
|
|
235
|
+
'MISSING_RECIPIENT',
|
|
236
|
+
{ messageType: message.type },
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const actor = this.#actors.get(sender);
|
|
240
|
+
if(!actor) {
|
|
241
|
+
throw new HttpTransportError(
|
|
242
|
+
`Unknown sender: ${sender}`,
|
|
243
|
+
'UNKNOWN_SENDER',
|
|
244
|
+
{ did: sender },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const envelope = signEnvelope(message, { did: sender, keys: actor.keys }, { to: recipient });
|
|
248
|
+
const dataJson = JSON.stringify(envelope);
|
|
249
|
+
|
|
250
|
+
const inbox = this.#getOrCreateInbox(recipient);
|
|
251
|
+
const stored = inbox.buffer.append(SSE_EVENT.MESSAGE, dataJson);
|
|
252
|
+
for(const sub of inbox.subscribers) {
|
|
253
|
+
this.#safeWrite(sub.stream, stored.event, stored.data, stored.id);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
publishRepeating(
|
|
258
|
+
message: BaseMessage,
|
|
259
|
+
sender: string,
|
|
260
|
+
_intervalMs: number,
|
|
261
|
+
_recipient?: string,
|
|
262
|
+
): () => void {
|
|
263
|
+
const actor = this.#actors.get(sender);
|
|
264
|
+
if(!actor) {
|
|
265
|
+
throw new HttpTransportError(`Unknown sender: ${sender}`, 'UNKNOWN_SENDER', { did: sender });
|
|
266
|
+
}
|
|
267
|
+
const envelope = signEnvelope(message, { did: sender, keys: actor.keys });
|
|
268
|
+
const dataJson = JSON.stringify(envelope);
|
|
269
|
+
const id = String(++this.#advertSeq);
|
|
270
|
+
const expiresAtMs = this.#now() + this.#advertTtlMs;
|
|
271
|
+
|
|
272
|
+
this.#currentAdvert = { dataJson, id, expiresAtMs };
|
|
273
|
+
for(const sub of this.#broadcastSubscribers) {
|
|
274
|
+
this.#safeWrite(sub.stream, SSE_EVENT.ADVERT, dataJson, id);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return (): void => {
|
|
278
|
+
if(this.#currentAdvert?.id === id) this.#currentAdvert = undefined;
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ----------------------------------------------------------------
|
|
283
|
+
// Sans-I/O HTTP surface
|
|
284
|
+
// ----------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Handle a POST / GET request (non-SSE). The caller dispatches SSE paths to
|
|
288
|
+
* {@link handleSse} instead. Returns a fully formed response; the caller's
|
|
289
|
+
* adapter turns it into an HTTP write.
|
|
290
|
+
*/
|
|
291
|
+
async handleRequest(req: HttpRequestLike): Promise<HttpResponseLike> {
|
|
292
|
+
const method = req.method.toUpperCase();
|
|
293
|
+
if(method === 'OPTIONS') return this.#respond(204, '', req);
|
|
294
|
+
|
|
295
|
+
const path = extractPath(req.url);
|
|
296
|
+
|
|
297
|
+
if(method === 'GET' && path === HTTP_ROUTE.WELL_KNOWN) {
|
|
298
|
+
return this.#respondJson(200, this.#wellKnownMetadata(), req);
|
|
299
|
+
}
|
|
300
|
+
if(method === 'POST' && path === HTTP_ROUTE.MESSAGES) {
|
|
301
|
+
return await this.#handleMessagesPost(req);
|
|
302
|
+
}
|
|
303
|
+
if(method === 'POST' && path === HTTP_ROUTE.ADVERTS) {
|
|
304
|
+
return await this.#handleAdvertsPost(req);
|
|
305
|
+
}
|
|
306
|
+
return this.#respondJson(404, { error: 'not_found' }, req);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Open an SSE stream for a GET request. The caller is responsible for
|
|
311
|
+
* flushing writes and propagating the `onClose` callback when the HTTP
|
|
312
|
+
* connection ends.
|
|
313
|
+
*/
|
|
314
|
+
handleSse(req: HttpRequestLike, stream: SseStream): void {
|
|
315
|
+
if(req.method.toUpperCase() !== 'GET') {
|
|
316
|
+
stream.close();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const path = extractPath(req.url);
|
|
320
|
+
|
|
321
|
+
if(path === HTTP_ROUTE.ADVERTS) {
|
|
322
|
+
this.#openBroadcastSubscription(stream);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const inboxMatch = matchInboxPath(path);
|
|
326
|
+
if(inboxMatch) {
|
|
327
|
+
this.#openInboxSubscription(req, stream, inboxMatch.did, path);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
stream.close();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ----------------------------------------------------------------
|
|
334
|
+
// Request handlers
|
|
335
|
+
// ----------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
async #handleMessagesPost(req: HttpRequestLike): Promise<HttpResponseLike> {
|
|
338
|
+
const envelope = parseJsonBody<SignedEnvelope>(req.body);
|
|
339
|
+
if(!envelope) return this.#respondJson(400, { error: 'invalid_json' }, req);
|
|
340
|
+
|
|
341
|
+
const senderPk = this.#resolveSenderPk(envelope.from);
|
|
342
|
+
if(!senderPk) {
|
|
343
|
+
return this.#respondJson(401, { error: 'unknown_sender' }, req);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
|
|
348
|
+
} catch(err) {
|
|
349
|
+
this.#logger.debug('POST /v1/messages: envelope verification failed:', err);
|
|
350
|
+
return this.#respondJson(401, { error: 'invalid_envelope' }, req);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if(!this.#nonceCache.store(envelope.from, envelope.nonce, envelope.timestamp)) {
|
|
354
|
+
return this.#respondJson(409, { error: 'replay' }, req);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if(!this.#rateLimiter.consume(envelope.from, this.#now())) {
|
|
358
|
+
return this.#respondJson(429, { error: 'rate_limited' }, req);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if(!envelope.to) {
|
|
362
|
+
return this.#respondJson(400, { error: 'missing_recipient' }, req);
|
|
363
|
+
}
|
|
364
|
+
const actor = this.#actors.get(envelope.to);
|
|
365
|
+
if(!actor) {
|
|
366
|
+
return this.#respondJson(404, { error: 'unknown_recipient' }, req);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const revived = reviveFromWire(envelope.message) as Record<string, unknown>;
|
|
370
|
+
const flat = flattenMessage(revived);
|
|
371
|
+
const messageType = typeof flat.type === 'string' ? flat.type : undefined;
|
|
372
|
+
if(!messageType) return this.#respondJson(400, { error: 'missing_message_type' }, req);
|
|
373
|
+
|
|
374
|
+
const handler = actor.handlers.get(messageType);
|
|
375
|
+
if(handler) {
|
|
376
|
+
try { await handler(flat); }
|
|
377
|
+
catch(err) {
|
|
378
|
+
this.#logger.debug(`Handler threw for ${messageType}:`, err);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return this.#respondJson(202, { ok: true }, req);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async #handleAdvertsPost(req: HttpRequestLike): Promise<HttpResponseLike> {
|
|
385
|
+
const envelope = parseJsonBody<SignedEnvelope>(req.body);
|
|
386
|
+
if(!envelope) return this.#respondJson(400, { error: 'invalid_json' }, req);
|
|
387
|
+
|
|
388
|
+
const senderPk = this.#resolveSenderPk(envelope.from);
|
|
389
|
+
if(!senderPk) return this.#respondJson(401, { error: 'unknown_sender' }, req);
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
|
|
393
|
+
} catch {
|
|
394
|
+
return this.#respondJson(401, { error: 'invalid_envelope' }, req);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if(!this.#nonceCache.store(envelope.from, envelope.nonce, envelope.timestamp)) {
|
|
398
|
+
return this.#respondJson(409, { error: 'replay' }, req);
|
|
399
|
+
}
|
|
400
|
+
if(!this.#rateLimiter.consume(envelope.from, this.#now())) {
|
|
401
|
+
return this.#respondJson(429, { error: 'rate_limited' }, req);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Only registered actors can publish adverts on this server.
|
|
405
|
+
if(!this.#actors.has(envelope.from)) {
|
|
406
|
+
return this.#respondJson(403, { error: 'not_an_actor' }, req);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const id = String(++this.#advertSeq);
|
|
410
|
+
this.#currentAdvert = {
|
|
411
|
+
dataJson : JSON.stringify(envelope),
|
|
412
|
+
id,
|
|
413
|
+
expiresAtMs : this.#now() + this.#advertTtlMs,
|
|
414
|
+
};
|
|
415
|
+
for(const sub of this.#broadcastSubscribers) {
|
|
416
|
+
this.#safeWrite(sub.stream, SSE_EVENT.ADVERT, this.#currentAdvert.dataJson, id);
|
|
417
|
+
}
|
|
418
|
+
return this.#respondJson(202, { ok: true }, req);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
#openBroadcastSubscription(stream: SseStream): void {
|
|
422
|
+
const sub: BroadcastSubscriber = { stream };
|
|
423
|
+
this.#broadcastSubscribers.add(sub);
|
|
424
|
+
stream.onClose(() => {
|
|
425
|
+
this.#closeBroadcastSubscriber(sub);
|
|
426
|
+
this.#broadcastSubscribers.delete(sub);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Replay current advert if still within TTL.
|
|
430
|
+
if(this.#currentAdvert && this.#currentAdvert.expiresAtMs > this.#now()) {
|
|
431
|
+
this.#safeWrite(stream, SSE_EVENT.ADVERT, this.#currentAdvert.dataJson, this.#currentAdvert.id);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if(this.#heartbeatMs > 0) {
|
|
435
|
+
sub.heartbeatTimer = setInterval(() => {
|
|
436
|
+
try { stream.writeComment('hb'); } catch { /* caller-owned failure */ }
|
|
437
|
+
}, this.#heartbeatMs);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#openInboxSubscription(req: HttpRequestLike, stream: SseStream, did: string, path: string): void {
|
|
442
|
+
const auth = req.headers.authorization;
|
|
443
|
+
if(!auth) {
|
|
444
|
+
this.#logger.debug(`Inbox subscribe: missing authorization header for ${did}`);
|
|
445
|
+
stream.close();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const senderPk = this.#resolveSenderPk(did);
|
|
449
|
+
if(!senderPk) {
|
|
450
|
+
stream.close();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
let parsedTs = 0;
|
|
454
|
+
let parsedNonce = '';
|
|
455
|
+
try {
|
|
456
|
+
const parsed = verifyRequestAuth(auth, path, senderPk, {
|
|
457
|
+
clockSkewSec : this.#clockSkewSec,
|
|
458
|
+
now : () => this.#now(),
|
|
459
|
+
});
|
|
460
|
+
if(parsed.did !== did) { stream.close(); return; }
|
|
461
|
+
parsedTs = parsed.ts;
|
|
462
|
+
parsedNonce = parsed.nonce;
|
|
463
|
+
} catch(err) {
|
|
464
|
+
this.#logger.debug(`Inbox subscribe: auth verification failed for ${did}:`, err);
|
|
465
|
+
stream.close();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if(!this.#nonceCache.store(did, parsedNonce, parsedTs)) {
|
|
469
|
+
stream.close();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if(!this.#rateLimiter.consume(did, this.#now())) {
|
|
473
|
+
stream.close();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const inbox = this.#getOrCreateInbox(did);
|
|
478
|
+
const sub: InboxSubscriber = { stream };
|
|
479
|
+
inbox.subscribers.add(sub);
|
|
480
|
+
stream.onClose(() => {
|
|
481
|
+
this.#closeInboxSubscriber(sub);
|
|
482
|
+
inbox.subscribers.delete(sub);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Replay buffered events since the client's Last-Event-ID, if any.
|
|
486
|
+
const lastEventId = req.headers['last-event-id'];
|
|
487
|
+
for(const stored of inbox.buffer.since(lastEventId)) {
|
|
488
|
+
this.#safeWrite(stream, stored.event, stored.data, stored.id);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if(this.#heartbeatMs > 0) {
|
|
492
|
+
sub.heartbeatTimer = setInterval(() => {
|
|
493
|
+
try { stream.writeComment('hb'); } catch { /* caller-owned failure */ }
|
|
494
|
+
}, this.#heartbeatMs);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ----------------------------------------------------------------
|
|
499
|
+
// Internal helpers
|
|
500
|
+
// ----------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
#getOrCreateInbox(did: string): InboxState {
|
|
503
|
+
let inbox = this.#inboxes.get(did);
|
|
504
|
+
if(!inbox) {
|
|
505
|
+
inbox = { buffer: new InboxBuffer(this.#inboxBufferSize), subscribers: new Set() };
|
|
506
|
+
this.#inboxes.set(did, inbox);
|
|
507
|
+
}
|
|
508
|
+
return inbox;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#resolveSenderPk(did: string): CompressedSecp256k1PublicKey | undefined {
|
|
512
|
+
const peerBytes = this.#peers.get(did);
|
|
513
|
+
if(peerBytes) {
|
|
514
|
+
try { return new CompressedSecp256k1PublicKey(peerBytes); }
|
|
515
|
+
catch { /* fall through */ }
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const components = Identifier.decode(did);
|
|
519
|
+
if(components.idType === 'KEY') {
|
|
520
|
+
return new CompressedSecp256k1PublicKey(components.genesisBytes);
|
|
521
|
+
}
|
|
522
|
+
} catch { /* not decodable */ }
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
#safeWrite(stream: SseStream, event: string, data: string, id?: string): void {
|
|
527
|
+
try { stream.writeEvent(event, data, id); }
|
|
528
|
+
catch(err) { this.#logger.debug('SSE writeEvent failed:', err); }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#closeBroadcastSubscriber(sub: BroadcastSubscriber): void {
|
|
532
|
+
if(sub.heartbeatTimer) clearInterval(sub.heartbeatTimer);
|
|
533
|
+
try { sub.stream.close(); } catch { /* already closed */ }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#closeInboxSubscriber(sub: InboxSubscriber): void {
|
|
537
|
+
if(sub.heartbeatTimer) clearInterval(sub.heartbeatTimer);
|
|
538
|
+
try { sub.stream.close(); } catch { /* already closed */ }
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
#respondJson(status: number, body: unknown, req: HttpRequestLike): HttpResponseLike {
|
|
542
|
+
return {
|
|
543
|
+
status,
|
|
544
|
+
headers : { 'content-type': 'application/json', ...this.#corsHeaders(req) },
|
|
545
|
+
body : JSON.stringify(body),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#respond(status: number, body: string, req: HttpRequestLike): HttpResponseLike {
|
|
550
|
+
return { status, headers: this.#corsHeaders(req), body };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#corsHeaders(req: HttpRequestLike): Record<string, string> {
|
|
554
|
+
const origin = req.headers.origin;
|
|
555
|
+
if(!origin) return {};
|
|
556
|
+
const common: Record<string, string> = {
|
|
557
|
+
'access-control-allow-methods' : 'GET, POST, OPTIONS',
|
|
558
|
+
'access-control-allow-headers' : 'authorization, content-type, last-event-id',
|
|
559
|
+
'access-control-max-age' : '86400',
|
|
560
|
+
};
|
|
561
|
+
switch(this.#cors.mode) {
|
|
562
|
+
case 'permissive':
|
|
563
|
+
return { 'access-control-allow-origin': '*', ...common };
|
|
564
|
+
case 'allowlist':
|
|
565
|
+
if(this.#cors.origins.includes(origin)) {
|
|
566
|
+
return { 'access-control-allow-origin': origin, vary: 'origin', ...common };
|
|
567
|
+
}
|
|
568
|
+
return {};
|
|
569
|
+
case 'same-origin':
|
|
570
|
+
return {};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#wellKnownMetadata(): Record<string, unknown> {
|
|
575
|
+
return {
|
|
576
|
+
envelopeVersion : HTTP_ENVELOPE_VERSION,
|
|
577
|
+
heartbeatIntervalMs : this.#heartbeatMs,
|
|
578
|
+
inboxBufferSize : this.#inboxBufferSize,
|
|
579
|
+
advertTtlMs : this.#advertTtlMs,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ----------------------------------------------------------------
|
|
585
|
+
// Module-local pure helpers
|
|
586
|
+
// ----------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
function extractPath(reqUrl: string): string {
|
|
589
|
+
if(reqUrl.startsWith('http://') || reqUrl.startsWith('https://')) {
|
|
590
|
+
return new URL(reqUrl).pathname;
|
|
591
|
+
}
|
|
592
|
+
const q = reqUrl.indexOf('?');
|
|
593
|
+
return q === -1 ? reqUrl : reqUrl.slice(0, q);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function matchInboxPath(path: string): { did: string } | undefined {
|
|
597
|
+
if(!path.startsWith(INBOX_PATH_PREFIX) || !path.endsWith(INBOX_PATH_SUFFIX)) return undefined;
|
|
598
|
+
const encodedDid = path.slice(INBOX_PATH_PREFIX.length, path.length - INBOX_PATH_SUFFIX.length);
|
|
599
|
+
if(!encodedDid) return undefined;
|
|
600
|
+
try { return { did: decodeURIComponent(encodedDid) }; }
|
|
601
|
+
catch { return undefined; }
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function parseJsonBody<T>(body: string | undefined): T | undefined {
|
|
605
|
+
if(body === undefined || body === '') return undefined;
|
|
606
|
+
try { return JSON.parse(body) as T; }
|
|
607
|
+
catch { return undefined; }
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function flattenMessage(msg: Record<string, unknown>): Record<string, unknown> {
|
|
611
|
+
if(msg.body && typeof msg.body === 'object') {
|
|
612
|
+
return { ...msg, ...(msg.body as Record<string, unknown>) };
|
|
613
|
+
}
|
|
614
|
+
return msg;
|
|
615
|
+
}
|