@opengeni/events 0.2.4 → 0.2.7
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/LICENSE +190 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +59 -16
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/coalesce.ts +41 -10
- package/src/index.ts +49 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 Cloudgeni-ai
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
package/dist/index.d.ts
CHANGED
|
@@ -108,6 +108,14 @@ declare function decodeAuthRequest(token: string): DecodedAuthRequest | null;
|
|
|
108
108
|
*/
|
|
109
109
|
declare function workspaceAgentPermissions(workspaceId: string): NatsPermissions;
|
|
110
110
|
|
|
111
|
+
type EventLogger = {
|
|
112
|
+
debug?: (message: string, attributes?: Record<string, unknown>) => void;
|
|
113
|
+
warn?: (message: string, attributes?: Record<string, unknown>) => void;
|
|
114
|
+
};
|
|
115
|
+
type EventBusOptions = {
|
|
116
|
+
logger?: EventLogger;
|
|
117
|
+
};
|
|
118
|
+
|
|
111
119
|
/**
|
|
112
120
|
* A raw request/reply reply — just the response bytes. Mirrors the subset of the
|
|
113
121
|
* NATS `Msg` shape a binary request/reply caller needs (`NatsControlRpc` consumes
|
|
@@ -174,6 +182,7 @@ type EventBus = {
|
|
|
174
182
|
* plane injects this so the transport never opens a second connection.
|
|
175
183
|
*/
|
|
176
184
|
getRequestConnection: () => RequestConnection;
|
|
185
|
+
isConnected?: () => boolean;
|
|
177
186
|
close: () => Promise<void>;
|
|
178
187
|
};
|
|
179
188
|
/**
|
|
@@ -187,7 +196,7 @@ type EventBus = {
|
|
|
187
196
|
declare function createNatsEventBus(natsUrl: string, auth?: {
|
|
188
197
|
user: string;
|
|
189
198
|
pass: string;
|
|
190
|
-
}): Promise<EventBus>;
|
|
199
|
+
}, options?: EventBusOptions): Promise<EventBus>;
|
|
191
200
|
/**
|
|
192
201
|
* A standalone NATS connection answering request/reply on ONE subject — the
|
|
193
202
|
* transport primitive the auth-callout responder uses. It is DELIBERATELY a
|
|
@@ -227,8 +236,9 @@ type NatsConnectAuth = {
|
|
|
227
236
|
*/
|
|
228
237
|
declare function createResponderConnection(natsUrl: string, auth: NatsConnectAuth, subject: string, handler: RequestHandler, options?: {
|
|
229
238
|
name?: string;
|
|
239
|
+
logger?: EventLogger;
|
|
230
240
|
}): Promise<ResponderConnection>;
|
|
231
241
|
declare function appendAndPublishEvents(db: Database, bus: EventBus, workspaceId: string, sessionId: string, events: AppendEventInput[]): Promise<SessionEvent[]>;
|
|
232
242
|
declare function formatSse(event: SessionEvent): string;
|
|
233
243
|
|
|
234
|
-
export { type DecodedAuthRequest, type EventBus, type MintAuthResponseInput, type MintUserJwtInput, type NatsConnectAuth, type NatsPermission, type NatsPermissions, type RequestConnection, type RequestHandler, type RequestReply, type ResponderConnection, appendAndPublishEvents, coalesceSessionEventDeltas, createNatsEventBus, createResponderConnection, decodeAuthRequest, formatSse, mintAuthResponse, mintUserJwt, workspaceAgentPermissions };
|
|
244
|
+
export { type DecodedAuthRequest, type EventBus, type EventBusOptions, type EventLogger, type MintAuthResponseInput, type MintUserJwtInput, type NatsConnectAuth, type NatsPermission, type NatsPermissions, type RequestConnection, type RequestHandler, type RequestReply, type ResponderConnection, appendAndPublishEvents, coalesceSessionEventDeltas, createNatsEventBus, createResponderConnection, decodeAuthRequest, formatSse, mintAuthResponse, mintUserJwt, workspaceAgentPermissions };
|
package/dist/index.js
CHANGED
|
@@ -17,10 +17,15 @@ function coalesceSessionEventDeltas(events) {
|
|
|
17
17
|
}
|
|
18
18
|
coalesced.push({
|
|
19
19
|
...run.first,
|
|
20
|
-
payload: {
|
|
21
|
-
|
|
20
|
+
payload: run.first.type === "sandbox.command.output.delta" ? {
|
|
21
|
+
chunk: run.text,
|
|
22
22
|
coalescedUntil: run.lastSequence,
|
|
23
|
-
...run.
|
|
23
|
+
...run.sandboxStream !== void 0 ? { stream: run.sandboxStream } : {},
|
|
24
|
+
...run.sandboxCommandId !== void 0 ? { commandId: run.sandboxCommandId } : {},
|
|
25
|
+
...run.sandboxName !== void 0 ? { name: run.sandboxName } : {}
|
|
26
|
+
} : {
|
|
27
|
+
text: run.text,
|
|
28
|
+
coalescedUntil: run.lastSequence
|
|
24
29
|
}
|
|
25
30
|
});
|
|
26
31
|
run = null;
|
|
@@ -31,8 +36,11 @@ function coalesceSessionEventDeltas(events) {
|
|
|
31
36
|
coalesced.push(event);
|
|
32
37
|
continue;
|
|
33
38
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
39
|
+
const isSandbox = event.type === "sandbox.command.output.delta";
|
|
40
|
+
const sandboxName = isSandbox ? sandboxDeltaName(event.payload) : void 0;
|
|
41
|
+
const sandboxStream = isSandbox ? sandboxDeltaString(event.payload, "stream") : void 0;
|
|
42
|
+
const sandboxCommandId = isSandbox ? sandboxDeltaString(event.payload, "commandId") : void 0;
|
|
43
|
+
if (run && sameDeltaRun(run.first, event, run.sandboxName, sandboxName) && run.sandboxStream === sandboxStream && run.sandboxCommandId === sandboxCommandId) {
|
|
36
44
|
run.text += deltaText(event);
|
|
37
45
|
run.lastSequence = event.sequence;
|
|
38
46
|
continue;
|
|
@@ -42,7 +50,9 @@ function coalesceSessionEventDeltas(events) {
|
|
|
42
50
|
first: event,
|
|
43
51
|
lastSequence: event.sequence,
|
|
44
52
|
text: deltaText(event),
|
|
45
|
-
sandboxName
|
|
53
|
+
sandboxName,
|
|
54
|
+
sandboxStream,
|
|
55
|
+
sandboxCommandId
|
|
46
56
|
};
|
|
47
57
|
}
|
|
48
58
|
flush();
|
|
@@ -66,7 +76,12 @@ function deltaText(event) {
|
|
|
66
76
|
}
|
|
67
77
|
const payload = asRecord(event.payload);
|
|
68
78
|
if (event.type === "sandbox.command.output.delta") {
|
|
69
|
-
|
|
79
|
+
for (const key of ["chunk", "text", "output"]) {
|
|
80
|
+
if (typeof payload[key] === "string") {
|
|
81
|
+
return payload[key];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return "";
|
|
70
85
|
}
|
|
71
86
|
return typeof payload.text === "string" ? payload.text : "";
|
|
72
87
|
}
|
|
@@ -88,6 +103,10 @@ function sandboxDeltaName(payload) {
|
|
|
88
103
|
const name = asRecord(payload).name;
|
|
89
104
|
return typeof name === "string" ? name : void 0;
|
|
90
105
|
}
|
|
106
|
+
function sandboxDeltaString(payload, key) {
|
|
107
|
+
const value = asRecord(payload)[key];
|
|
108
|
+
return typeof value === "string" ? value : void 0;
|
|
109
|
+
}
|
|
91
110
|
function asRecord(value) {
|
|
92
111
|
return value && typeof value === "object" ? value : {};
|
|
93
112
|
}
|
|
@@ -241,6 +260,12 @@ function workspaceAgentPermissions(workspaceId) {
|
|
|
241
260
|
// src/index.ts
|
|
242
261
|
import { connect as connect2, nkeys as nkeys2 } from "nats";
|
|
243
262
|
var codec = JSONCodec();
|
|
263
|
+
var silentLogger = {
|
|
264
|
+
debug: () => {
|
|
265
|
+
},
|
|
266
|
+
warn: () => {
|
|
267
|
+
}
|
|
268
|
+
};
|
|
244
269
|
var RECONNECT_OPTIONS = {
|
|
245
270
|
reconnect: true,
|
|
246
271
|
maxReconnectAttempts: -1,
|
|
@@ -268,24 +293,40 @@ async function flushWithTimeout(nc, timeoutMs) {
|
|
|
268
293
|
}
|
|
269
294
|
}
|
|
270
295
|
}
|
|
271
|
-
function logConnectionStatus(nc, label) {
|
|
296
|
+
function logConnectionStatus(nc, label, logger = silentLogger, onStatus) {
|
|
272
297
|
void (async () => {
|
|
273
298
|
try {
|
|
274
299
|
for await (const status of nc.status()) {
|
|
275
|
-
|
|
300
|
+
onStatus?.(status.type);
|
|
301
|
+
const attributes = { label, status: status.type, data: status.data };
|
|
302
|
+
if (isWarnNatsStatus(status.type)) {
|
|
303
|
+
(logger.warn ?? silentLogger.warn)("NATS connection status", attributes);
|
|
304
|
+
} else {
|
|
305
|
+
(logger.debug ?? silentLogger.debug)("NATS connection status", attributes);
|
|
306
|
+
}
|
|
276
307
|
}
|
|
277
308
|
} catch {
|
|
278
309
|
}
|
|
279
310
|
})();
|
|
280
311
|
}
|
|
281
|
-
|
|
312
|
+
function isWarnNatsStatus(type) {
|
|
313
|
+
return type === "disconnect" || type === "error" || type === "staleConnection";
|
|
314
|
+
}
|
|
315
|
+
async function createNatsEventBus(natsUrl, auth, options = {}) {
|
|
282
316
|
const connectOptions = { servers: natsUrl };
|
|
283
317
|
if (auth) {
|
|
284
318
|
connectOptions.user = auth.user;
|
|
285
319
|
connectOptions.pass = auth.pass;
|
|
286
320
|
}
|
|
287
321
|
const nc = await connect(withReconnectDefaults(connectOptions));
|
|
288
|
-
|
|
322
|
+
let connected = true;
|
|
323
|
+
logConnectionStatus(nc, "event-bus", options.logger, (type) => {
|
|
324
|
+
if (type === "disconnect" || type === "reconnecting" || type === "staleConnection" || type === "error") {
|
|
325
|
+
connected = false;
|
|
326
|
+
} else if (type === "connect" || type === "reconnect") {
|
|
327
|
+
connected = true;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
289
330
|
const requestConnection = {
|
|
290
331
|
request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeout)
|
|
291
332
|
};
|
|
@@ -297,10 +338,11 @@ async function createNatsEventBus(natsUrl, auth) {
|
|
|
297
338
|
try {
|
|
298
339
|
nc.publish(sessionSubject(workspaceId, sessionId), codec.encode({ workspaceId, sessionId, events }));
|
|
299
340
|
} catch (error) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
341
|
+
(options.logger?.warn ?? silentLogger.warn)("NATS live publish dropped; events are durable in the DB and reconcile on stream replay", {
|
|
342
|
+
workspaceId,
|
|
343
|
+
sessionId,
|
|
344
|
+
error: error instanceof Error ? error.message : String(error)
|
|
345
|
+
});
|
|
304
346
|
return;
|
|
305
347
|
}
|
|
306
348
|
await flushWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);
|
|
@@ -310,6 +352,7 @@ async function createNatsEventBus(natsUrl, auth) {
|
|
|
310
352
|
subscribeRequests: (subject, handler) => subscribeRequests(nc, subject, handler),
|
|
311
353
|
subscribeAgentEvents: (subject, handler) => subscribeAgentEvents(nc, subject, handler),
|
|
312
354
|
getRequestConnection: () => requestConnection,
|
|
355
|
+
isConnected: () => connected && !nc.isClosed() && !nc.isDraining(),
|
|
313
356
|
close: async () => {
|
|
314
357
|
await nc.drain();
|
|
315
358
|
}
|
|
@@ -327,7 +370,7 @@ async function createResponderConnection(natsUrl, auth, subject, handler, option
|
|
|
327
370
|
connectOptions.token = auth.token;
|
|
328
371
|
}
|
|
329
372
|
const nc = await connect(withReconnectDefaults(connectOptions));
|
|
330
|
-
logConnectionStatus(nc, options.name ? `auth-callout:${options.name}` : "auth-callout");
|
|
373
|
+
logConnectionStatus(nc, options.name ? `auth-callout:${options.name}` : "auth-callout", options.logger);
|
|
331
374
|
const sub = nc.subscribe(subject);
|
|
332
375
|
void (async () => {
|
|
333
376
|
for await (const msg of sub) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/coalesce.ts","../src/nats-jwt.ts"],"sourcesContent":["import type { SessionBusMessage, SessionEvent } from \"@opengeni/contracts\";\nimport { appendSessionEvents, sessionSubject, type AppendEventInput, type Database } from \"@opengeni/db\";\nimport { connect, JSONCodec, type ConnectionOptions, type Msg, type NatsConnection, type Subscription } from \"nats\";\n\nconst codec = JSONCodec<SessionBusMessage | SessionEvent>();\n\nexport { coalesceSessionEventDeltas } from \"./coalesce\";\n\n/**\n * Reconnect + keepalive defaults applied to EVERY long-lived NATS connection\n * this package opens (the event bus AND the standalone auth-callout responder).\n *\n * The production outage these guard against: an in-cluster NATS broker pod\n * restart. nats.js's stock policy gives up after ~10 attempts (~20s) and the\n * client goes permanently CONNECTION_CLOSED — which takes the whole control\n * plane down with it: every session-create publishes events to NATS, and the\n * API-hosted auth-callout responder dies so BYO agents get \"authorization\n * violation\". Recovery then required a MANUAL api+worker restart. With these\n * options the client retries forever and auto-recovers the moment the broker\n * returns. Factored into one source of truth so the call sites never drift.\n *\n * - `reconnect` + `maxReconnectAttempts: -1` — never give up (infinite retry).\n * - `reconnectTimeWait` (2s base) + `reconnectJitter`/`reconnectJitterTLS`\n * (up to 1s) — a fleet of api/worker pods doesn't thundering-herd the broker\n * on recovery.\n * - `waitOnFirstConnect` — a broker briefly unavailable at boot must not\n * hard-fail the process; the client keeps trying instead of throwing.\n * - `pingInterval`/`maxPingOut` — promptly detect a silently-dead socket so the\n * reconnect machinery actually engages instead of hanging on a zombie.\n */\nconst RECONNECT_OPTIONS = {\n reconnect: true,\n maxReconnectAttempts: -1,\n reconnectTimeWait: 2_000,\n reconnectJitter: 1_000,\n reconnectJitterTLS: 1_000,\n waitOnFirstConnect: true,\n pingInterval: 20_000,\n maxPingOut: 3,\n} satisfies ConnectionOptions;\n\n/**\n * The single source of truth for a long-lived connection's resilience: merge the\n * reconnect/keepalive defaults UNDER the caller's connection options (servers +\n * optional auth/name). Every long-lived `connect()` in this package goes through\n * here so the two call sites can never diverge.\n */\nfunction withReconnectDefaults(options: ConnectionOptions): ConnectionOptions {\n return { ...RECONNECT_OPTIONS, ...options };\n}\n\n/** How long a best-effort publish waits on `flush()` before giving up (see `publish`). */\nconst PUBLISH_FLUSH_TIMEOUT_MS = 2_000;\n\n/**\n * Await `nc.flush()` but never longer than `timeoutMs`. With infinite reconnect a\n * `flush()` issued while the broker is down does NOT reject — it pends until the\n * broker returns, which can be minutes. Racing it against a timer keeps a long\n * outage from stalling an in-flight turn; the published message stays buffered\n * and is delivered on reconnect regardless. A flush rejection (connection fully\n * CLOSED) is swallowed here so the timeout race never leaks an unhandled\n * rejection — the caller's publish path is what logs the drop.\n */\nasync function flushWithTimeout(nc: NatsConnection, timeoutMs: number): Promise<void> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs);\n });\n try {\n await Promise.race([nc.flush().catch(() => undefined), timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\n/**\n * Drain a long-lived connection's status async-iterator to the log so a future\n * broker outage is OBSERVABLE (disconnect → reconnecting → reconnect → update).\n * Fire-and-forget for the connection's lifetime; the loop ends when the\n * connection closes. `label` distinguishes the event-bus connection from the\n * auth-callout responder in the logs.\n */\nfunction logConnectionStatus(nc: NatsConnection, label: string): void {\n void (async () => {\n try {\n for await (const status of nc.status()) {\n console.warn(`[nats:${label}] ${status.type}`, status.data);\n }\n } catch {\n // The status iterator simply ends when the connection closes; never let it\n // throw out of this background loop.\n }\n })();\n}\n\nexport {\n decodeAuthRequest,\n mintAuthResponse,\n mintUserJwt,\n workspaceAgentPermissions,\n type DecodedAuthRequest,\n type MintAuthResponseInput,\n type MintUserJwtInput,\n type NatsPermission,\n type NatsPermissions,\n} from \"./nats-jwt\";\n\n// Re-export the raw NATS primitives a consumer needs to open a direct connection or\n// generate nkeys (the auth-callout responder's standalone connection, the\n// agent-simulating integration tests). This keeps `nats` an internal dependency of\n// this leaf — callers in the bun workspace reach it through @opengeni/events rather\n// than depending on `nats` directly.\nexport { connect, nkeys, type NatsConnection } from \"nats\";\n\n/**\n * A raw request/reply reply — just the response bytes. Mirrors the subset of the\n * NATS `Msg` shape a binary request/reply caller needs (`NatsControlRpc` consumes\n * exactly this). Kept minimal so the events package does not leak the `nats` `Msg`\n * type into the agent-loop-free runtime leaf.\n */\nexport type RequestReply = { data: Uint8Array };\n\n/**\n * The minimal request/reply connection the selfhosted control plane consumes\n * (structurally identical to `@opengeni/runtime`'s `NatsRequestConnection`). The\n * API/worker hand this accessor to `NatsControlRpc` so the control transport rides\n * the SAME managed NATS connection the event bus already owns — a NATS connection\n * natively supports both pub/sub and request/reply, so there is NEVER a second\n * connection.\n */\nexport interface RequestConnection {\n request(subject: string, payload: Uint8Array, opts: { timeout: number }): Promise<RequestReply>;\n}\n\n/**\n * A handler answering a request/reply on a subscribed subject: given the request\n * bytes (+ the concrete subject the message landed on, for `agent.<ws>.<id>.rpc`\n * style wildcard routing), return the response bytes to reply with. A thrown error\n * leaves the request unanswered (the caller's request times out / sees no\n * responder), which the control plane maps to `agent_offline` / reconnecting.\n */\nexport type RequestHandler = (request: Uint8Array, subject: string) => Promise<Uint8Array> | Uint8Array;\n\nexport type EventBus = {\n publish: (workspaceId: string, sessionId: string, events: SessionEvent[]) => Promise<void>;\n subscribe: (workspaceId: string, sessionId: string, onEvents: (events: SessionEvent[]) => void | Promise<void>) => Promise<() => void>;\n /**\n * Issue a binary request/reply on a subject over the bus's NATS connection\n * (the selfhosted control plane: `agent.<ws>.<id>.rpc`). A new usage of what was\n * a one-way bus — same connection, native NATS request/reply. Rejects on a\n * no-responder (NATS 503) or a request timeout; the caller (`NatsControlRpc`)\n * maps those to `agent_offline` / `agent_reconnecting`, never a NotFound.\n */\n request: (subject: string, payload: Uint8Array, opts: { timeoutMs: number }) => Promise<RequestReply>;\n /**\n * Subscribe-and-reply on a subject (the responder side — the enrolled agent, or\n * a test stand-in for it): for every request on `subject`, call `handler` and\n * `respond` with its bytes over the SAME connection. Returns an unsubscribe fn.\n * A subject may be a NATS wildcard (e.g. `agent.*.*.rpc`).\n */\n subscribeRequests: (subject: string, handler: RequestHandler) => () => void;\n /**\n * Subscribe to the agent EVENT plane (the one-way fire-and-forget heartbeats +\n * going-offline the agent PUBLISHES on `agent.<ws>.<id>.events`, NOT a\n * request/reply). The M10 metrics-ingestion consumer subscribes the wildcard\n * `agent.*.*.events` and gets each raw payload plus its concrete subject (so it\n * can extract `<ws>`/`<id>` for the per-enrollment upsert). Returns an\n * unsubscribe fn. Decoding the AgentEvent is the caller's concern (this leaf\n * does not depend on `@opengeni/agent-proto`).\n */\n subscribeAgentEvents: (\n subject: string,\n handler: (payload: Uint8Array, subject: string) => void | Promise<void>,\n ) => () => void;\n /**\n * The `RequestConnection` accessor the selfhosted `NatsControlRpc` consumes —\n * the SAME managed connection (pub/sub + request/reply share it). The control\n * plane injects this so the transport never opens a second connection.\n */\n getRequestConnection: () => RequestConnection;\n close: () => Promise<void>;\n};\n\n/**\n * Connect the event bus + control-plane request/reply over ONE managed NATS\n * connection. `auth` is the PRIVILEGED control-plane login (M-AUTH): when the\n * server runs with auth_callout, the api/worker authenticates as a static account\n * user permitted to request `agent.*.rpc` + receive its inbox replies. When `auth`\n * is omitted the connection is anonymous (local dev / a NATS without auth_callout)\n * — the existing behavior, unchanged.\n */\nexport async function createNatsEventBus(\n natsUrl: string,\n auth?: { user: string; pass: string },\n): Promise<EventBus> {\n const connectOptions: ConnectionOptions = { servers: natsUrl };\n if (auth) {\n connectOptions.user = auth.user;\n connectOptions.pass = auth.pass;\n }\n const nc = await connect(withReconnectDefaults(connectOptions));\n logConnectionStatus(nc, \"event-bus\");\n const requestConnection: RequestConnection = {\n request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeout),\n };\n return {\n publish: async (workspaceId, sessionId, events) => {\n if (events.length === 0) {\n return;\n }\n // Best-effort LIVE fan-out. These events are ALREADY durably appended to\n // the DB before we get here (they carry a DB-assigned `sequence`), and\n // every consumer reconciles from that durable log — the server SSE stream\n // replays + gap-backfills via `listSessionEvents`, and the SDK client\n // reconnects and replays from the durable events endpoint. So a publish\n // that fails during a broker blip only delays LIVE delivery (healed by the\n // next successful publish's gap-backfill, or a stream reconnect); it must\n // never throw the in-flight turn to death.\n try {\n nc.publish(sessionSubject(workspaceId, sessionId), codec.encode({ workspaceId, sessionId, events }));\n } catch (error) {\n // `publish()` throws synchronously only when the connection is fully\n // CLOSED (with infinite reconnect, effectively never outside shutdown).\n console.warn(\n `[nats:event-bus] dropped live publish for ${workspaceId}/${sessionId}; events are durable in the DB and reconcile on stream replay`,\n error,\n );\n return;\n }\n await flushWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);\n },\n subscribe: async (workspaceId, sessionId, onEvents) => subscribeSession(nc, workspaceId, sessionId, onEvents),\n request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeoutMs),\n subscribeRequests: (subject, handler) => subscribeRequests(nc, subject, handler),\n subscribeAgentEvents: (subject, handler) => subscribeAgentEvents(nc, subject, handler),\n getRequestConnection: () => requestConnection,\n close: async () => {\n await nc.drain();\n },\n };\n}\n\n/**\n * A standalone NATS connection answering request/reply on ONE subject — the\n * transport primitive the auth-callout responder uses. It is DELIBERATELY a\n * SEPARATE connection from the event bus: the callout responder authenticates as\n * the callout account's `auth_users` user (a username/password or token in the\n * `AUTH` account), which is a DIFFERENT identity from the control-plane's\n * privileged account that the event bus + `NatsControlRpc` ride. One connection\n * per identity; never multiplex the two.\n *\n * `request`/`reply` here is the RAW NATS request/reply (`$SYS.REQ.USER.AUTH`): the\n * server publishes an authorization request with a reply inbox; the handler returns\n * the signed authorization-response bytes which we `respond` on that inbox.\n */\nexport interface ResponderConnection {\n /** Subscribe-and-reply on `subject`; returns an async close that drains. */\n close: () => Promise<void>;\n}\n\n/** Connection auth for a standalone NATS connection (the callout responder). */\nexport type NatsConnectAuth =\n | { kind: \"user-password\"; user: string; pass: string }\n | { kind: \"token\"; token: string }\n | { kind: \"anonymous\" };\n\n/**\n * Open a standalone NATS connection and subscribe `subject`, replying to every\n * request with `handler(requestBytes, subject)`. Used by the auth-callout\n * responder to serve `$SYS.REQ.USER.AUTH` as the callout auth user. Returns a\n * handle whose `close()` drains the connection. A handler that throws leaves the\n * request UNANSWERED — for auth-callout that means the server denies the\n * connection on its own timeout, which is the correct fail-closed behavior (a\n * responder bug must never accidentally grant access).\n */\nexport async function createResponderConnection(\n natsUrl: string,\n auth: NatsConnectAuth,\n subject: string,\n handler: RequestHandler,\n options: { name?: string } = {},\n): Promise<ResponderConnection> {\n const connectOptions: ConnectionOptions = { servers: natsUrl };\n if (options.name) {\n connectOptions.name = options.name;\n }\n if (auth.kind === \"user-password\") {\n connectOptions.user = auth.user;\n connectOptions.pass = auth.pass;\n } else if (auth.kind === \"token\") {\n connectOptions.token = auth.token;\n }\n const nc = await connect(withReconnectDefaults(connectOptions));\n logConnectionStatus(nc, options.name ? `auth-callout:${options.name}` : \"auth-callout\");\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n if (!msg.reply) {\n continue;\n }\n try {\n const reply = await handler(msg.data, msg.subject);\n msg.respond(reply);\n } catch {\n // Leave UNANSWERED — fail-closed. The server denies the connect attempt\n // on its callout timeout; a responder error never grants access.\n }\n }\n })();\n return {\n close: async () => {\n sub.unsubscribe();\n await nc.drain();\n },\n };\n}\n\nexport async function appendAndPublishEvents(db: Database, bus: EventBus, workspaceId: string, sessionId: string, events: AppendEventInput[]): Promise<SessionEvent[]> {\n const appended = await appendSessionEvents(db, workspaceId, sessionId, events);\n // The DB append above is the durable system of record; the publish is only a\n // best-effort LIVE fan-out. Guard it so NO EventBus implementation can throw an\n // in-flight agent turn to death on a transient NATS disconnect — consumers\n // reconcile any missed live events from the durable log via the events/stream\n // endpoint (DB replay + gap-backfill). The managed `createNatsEventBus` bus\n // already swallows internally, so this catch is the belt-and-suspenders guard\n // for any other bus impl (and a fully CLOSED connection during shutdown).\n try {\n await bus.publish(workspaceId, sessionId, appended);\n } catch (error) {\n console.warn(\n `[events] live publish failed for ${workspaceId}/${sessionId}; ${appended.length} event(s) are durable and reconcile on stream replay`,\n error,\n );\n }\n return appended;\n}\n\nfunction subscribeSession(nc: NatsConnection, workspaceId: string, sessionId: string, onEvents: (events: SessionEvent[]) => void | Promise<void>): () => void {\n const sub: Subscription = nc.subscribe(sessionSubject(workspaceId, sessionId));\n void (async () => {\n for await (const msg of sub) {\n const decoded = codec.decode(msg.data) as SessionBusMessage | SessionEvent;\n const events = \"events\" in decoded ? decoded.events : [decoded];\n await onEvents(events);\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\n/**\n * A binary request/reply over the managed connection. Returns ONLY the reply\n * bytes (the `RequestReply` shape) — the request/reply error semantics (a\n * no-responder NATS 503, a request timeout) propagate as the rejected promise so\n * the caller owns the mapping. The reply is delivered via the connection's\n * built-in mux inbox; no extra subscription is created here.\n */\nasync function requestReply(nc: NatsConnection, subject: string, payload: Uint8Array, timeout: number): Promise<RequestReply> {\n const msg: Msg = await nc.request(subject, payload, { timeout });\n return { data: msg.data };\n}\n\n/**\n * Subscribe to `subject` and reply to every request with the handler's bytes,\n * over the SAME connection. The responder side of request/reply: each delivered\n * `Msg` carries a `reply` inbox; `msg.respond(bytes)` publishes the answer there.\n * A handler that throws (or a message with no `reply` subject) is left unanswered\n * — the requester then sees a timeout, never a malformed reply.\n */\nfunction subscribeRequests(nc: NatsConnection, subject: string, handler: RequestHandler): () => void {\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n // A request always carries a reply inbox; a plain publish to this subject\n // (no reply) is ignored — request/reply is the only contract here.\n if (!msg.reply) {\n continue;\n }\n try {\n const reply = await handler(msg.data, msg.subject);\n msg.respond(reply);\n } catch {\n // Leave the request unanswered: the requester's request times out, which\n // the selfhosted control plane reads as a transient blip (reconnecting),\n // never a malformed reply. The responder stays subscribed for the next op.\n }\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\n/**\n * Subscribe to the one-way agent event plane: deliver each published payload (the\n * agent's `AgentEvent` heartbeat / going-offline, NOT a request/reply) to the\n * handler with its concrete subject. A plain `nc.subscribe` (no reply); a handler\n * that throws is swallowed so one bad event never tears down the subscription\n * (ingestion is best-effort — a metrics gap is never fatal).\n */\nfunction subscribeAgentEvents(\n nc: NatsConnection,\n subject: string,\n handler: (payload: Uint8Array, subject: string) => void | Promise<void>,\n): () => void {\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n try {\n await handler(msg.data, msg.subject);\n } catch {\n // Swallow: best-effort ingestion. The subscription stays live for the\n // next event.\n }\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\nexport function formatSse(event: SessionEvent): string {\n return [\n `id: ${event.sequence}`,\n `event: ${event.type}`,\n `data: ${JSON.stringify(event)}`,\n \"\",\n \"\",\n ].join(\"\\n\");\n}\n","import type { SessionEvent } from \"@opengeni/contracts\";\n\nconst COALESCIBLE_DELTA_TYPES = new Set([\n \"agent.message.delta\",\n \"agent.reasoning.delta\",\n \"sandbox.command.output.delta\",\n]);\n\ntype DeltaRun = {\n first: SessionEvent;\n lastSequence: number;\n text: string;\n sandboxName: string | undefined;\n};\n\nexport function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent[] {\n const coalesced: SessionEvent[] = [];\n let run: DeltaRun | null = null;\n\n const flush = () => {\n if (!run) {\n return;\n }\n coalesced.push({\n ...run.first,\n payload: {\n text: run.text,\n coalescedUntil: run.lastSequence,\n ...(run.first.type === \"sandbox.command.output.delta\" && run.sandboxName !== undefined\n ? { name: run.sandboxName }\n : {}),\n },\n });\n run = null;\n };\n\n for (const event of events) {\n if (!isCoalescibleDelta(event)) {\n flush();\n coalesced.push(event);\n continue;\n }\n\n const sandboxName = event.type === \"sandbox.command.output.delta\" ? sandboxDeltaName(event.payload) : undefined;\n if (run && sameDeltaRun(run.first, event, run.sandboxName, sandboxName)) {\n run.text += deltaText(event);\n run.lastSequence = event.sequence;\n continue;\n }\n\n flush();\n run = {\n first: event,\n lastSequence: event.sequence,\n text: deltaText(event),\n sandboxName,\n };\n }\n\n flush();\n return coalesced;\n}\n\nfunction isCoalescibleDelta(event: SessionEvent): boolean {\n return COALESCIBLE_DELTA_TYPES.has(event.type);\n}\n\nfunction sameDeltaRun(\n first: SessionEvent,\n next: SessionEvent,\n firstSandboxName: string | undefined,\n nextSandboxName: string | undefined,\n): boolean {\n if (first.type !== next.type) {\n return false;\n }\n if ((first.turnId ?? null) !== (next.turnId ?? null)) {\n return false;\n }\n return first.type !== \"sandbox.command.output.delta\" || firstSandboxName === nextSandboxName;\n}\n\nfunction deltaText(event: SessionEvent): string {\n if (event.type === \"agent.reasoning.delta\") {\n return reasoningText(event.payload);\n }\n const payload = asRecord(event.payload);\n if (event.type === \"sandbox.command.output.delta\") {\n return typeof payload.text === \"string\" ? payload.text : typeof payload.output === \"string\" ? payload.output : \"\";\n }\n return typeof payload.text === \"string\" ? payload.text : \"\";\n}\n\nfunction reasoningText(payload: unknown): string {\n const record = asRecord(payload);\n if (typeof record.text === \"string\") {\n return record.text;\n }\n const content = asRecord(asRecord(record.item).rawItem).content;\n if (!Array.isArray(content)) {\n return \"\";\n }\n return content\n .map((part) => {\n const text = asRecord(part).text;\n return typeof text === \"string\" ? text : \"\";\n })\n .join(\"\");\n}\n\nfunction sandboxDeltaName(payload: unknown): string | undefined {\n const name = asRecord(payload).name;\n return typeof name === \"string\" ? name : undefined;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" ? value as Record<string, unknown> : {};\n}\n","// packages/events/src/nats-jwt.ts — NATS JWT v2 signing for the auth-callout\n// responder (bring-your-own-compute M-AUTH; dossier §10.1 NATS Accounts per\n// workspace + §17 the isolation smoke).\n//\n// This is the cryptographic core of the auth-callout tenancy boundary. When an\n// external agent connects to NATS presenting its `oge_` enrollment bearer as the\n// connect auth-token, nats-server (configured with `auth_callout`) issues an\n// authorization request on `$SYS.REQ.USER.AUTH`. Our responder (auth-callout.ts)\n// validates the bearer and answers with a SIGNED authorization-response JWT that\n// embeds a SIGNED user JWT scoping the connection to publish/subscribe ONLY\n// `agent.<workspaceId>.>` (+ the reply `_INBOX.>`). That per-subject permission\n// set IS the per-workspace isolation: workspace A's agent literally cannot\n// pub/sub workspace B's subjects (§19 the NATS-Accounts-misconfig leak risk is\n// closed at the JWT-permission layer, not just by subject naming).\n//\n// WHY HAND-ROLL THE JWT ENCODING (vs a dep): the NATS JWT v2 wire format is small,\n// stable, and fully specified (ADR-26 + nats-io/jwt): a base64url header\n// `{\"typ\":\"JWT\",\"alg\":\"ed25519-nkey\"}`, base64url JSON claims whose `jti` is the\n// base32(SHA-512/256(claims-with-blank-jti)), and an ed25519 nkey signature over\n// `header.payload`. nkeys (re-exported by the `nats` package we already depend on)\n// gives us the ed25519 sign primitive; Node `crypto` gives SHA-512/256. So we own\n// the encoding in a few well-tested functions rather than pull an alpha\n// `@nats-io/jwt` (0.0.x) whose nkeys-version compat is uncertain. No `xkey`\n// encryption is used (the bearer is already an authenticated identity claim and\n// the wire is TLS — encryption is an optional ADR-26 hardening, off here).\n//\n// SECURITY: the account SIGNING SEED never leaves this process and is NEVER logged.\n// Callers pass it as a `string` seed; we `fromSeed` it once per sign. The bearer\n// the responder validates is HMAC-verified elsewhere (verifyEnrollmentBearer); this\n// module only mints the scoped NATS credential once identity is proven.\n\nimport { createHash } from \"node:crypto\";\nimport { nkeys } from \"nats\";\n\n/** The NATS JWT v2 header — constant for every token we mint (ADR-26 / nats-io/jwt:\n * `TokenTypeJwt=\"JWT\"`, `AlgorithmNkey=\"ed25519-nkey\"`). */\nconst JWT_HEADER = { typ: \"JWT\", alg: \"ed25519-nkey\" } as const;\n\n/** NATS user-claim `nats.type` discriminator + `nats.version` for v2 claims. */\nconst USER_CLAIM_TYPE = \"user\";\nconst AUTH_RESPONSE_CLAIM_TYPE = \"authorization_response\";\nconst NATS_CLAIM_VERSION = 2;\n\n/** A NATS permission set: subject allow/deny lists (ADR-26 `pub`/`sub` →\n * `allow`/`deny`). An empty/undefined list means \"no explicit grant\" — combined\n * with the agent scope below, the connection can ONLY reach what `allow` lists. */\nexport interface NatsPermission {\n allow?: string[];\n deny?: string[];\n}\n\n/** The pub/sub permissions embedded in a user JWT. */\nexport interface NatsPermissions {\n pub: NatsPermission;\n sub: NatsPermission;\n}\n\n/**\n * The minimal nkey keypair surface this module needs — exactly what\n * `nkeys.fromSeed(seed)` returns. Declared structurally so the module does not\n * leak the `nats` nkeys type through its public signature.\n */\ninterface NkeyPair {\n getPublicKey(): string;\n sign(input: Uint8Array): Uint8Array;\n}\n\n/** base64url (RawURLEncoding — no padding), matching nats-io/jwt's `serialize`. */\nfunction base64UrlEncode(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString(\"base64url\");\n}\n\n/** RFC 4648 base32 (standard alphabet, NO padding) — the encoding nats-io/jwt\n * uses for the `jti` hash. Node has no built-in base32, so a tiny encoder. */\nconst BASE32_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\nfunction base32NoPadding(bytes: Uint8Array): string {\n let bits = 0;\n let value = 0;\n let out = \"\";\n for (const byte of bytes) {\n value = (value << 8) | byte;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n out += BASE32_ALPHABET[(value >>> bits) & 31];\n }\n }\n if (bits > 0) {\n out += BASE32_ALPHABET[(value << (5 - bits)) & 31];\n }\n return out;\n}\n\n/**\n * Compute the canonical NATS `jti`: base32(NoPadding, std-alphabet) of the\n * SHA-512/256 of the claims object SERIALIZED WITH AN EMPTY `jti` (nats-io/jwt's\n * `hash`). nats-server recomputes + verifies this on decode, so it must match\n * byte-for-byte. We serialize the SAME object we will sign, only with `jti:\"\"`.\n */\nfunction computeJti(claimsWithBlankJti: object): string {\n const json = JSON.stringify(claimsWithBlankJti);\n const digest = createHash(\"sha512-256\").update(json, \"utf8\").digest();\n return base32NoPadding(digest);\n}\n\n/**\n * Encode + sign a NATS v2 JWT. The `claims` MUST already carry `iss`/`sub`/`iat`\n * (+ optional `aud`/`exp`) and a `nats` block; this function fills `jti` (the\n * canonical hash), serializes `header.payload`, signs that with `signingKey`, and\n * appends the base64url signature. Returns the compact `header.payload.signature`.\n */\nfunction encodeJwt(claims: Record<string, unknown>, signingKey: NkeyPair): string {\n // jti is the hash of the claims with jti blanked — set it blank, hash, then set.\n const withBlankJti = { ...claims, jti: \"\" };\n const jti = computeJti(withBlankJti);\n const finalClaims = { ...claims, jti };\n\n const header = base64UrlEncode(Buffer.from(JSON.stringify(JWT_HEADER), \"utf8\"));\n const payload = base64UrlEncode(Buffer.from(JSON.stringify(finalClaims), \"utf8\"));\n const signingInput = `${header}.${payload}`;\n const signature = signingKey.sign(Buffer.from(signingInput, \"utf8\"));\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\n/**\n * Input to mint a workspace-scoped NATS user JWT for an enrolled agent.\n * - `userPublicKey` — the `user_nkey` from the authorization request; it MUST be\n * the `sub` of the user JWT (nats-server rejects a mismatch).\n * - `accountSeed` — the callout account SIGNING seed (`SA...`); both the user JWT\n * `iss` (its public key) and the signature come from it. NEVER logged.\n * - `name` — a human label for the user (the agent id), for server logs.\n * - `permissions` — the pub/sub allow/deny lists (the workspace scope).\n * - `expiresAtSeconds` — optional absolute `exp` (unix seconds). When set the\n * server will expire the connection's credential; we tie it to the bearer's\n * remaining life so a revoked/expired enrollment cannot outlive its bearer.\n */\nexport interface MintUserJwtInput {\n userPublicKey: string;\n accountSeed: string;\n name: string;\n permissions: NatsPermissions;\n /** The target account NAME (the `auth_callout.account`) the user binds to; the\n * embedded user JWT's `aud` in server-config mode. */\n audienceAccount: string;\n expiresAtSeconds?: number;\n}\n\n/**\n * Mint a signed NATS user JWT scoped by `permissions`. In auth-callout SERVER\n * mode the user JWT is signed by the callout ISSUER ACCOUNT key, and its `iss` is\n * that account's public key. The returned JWT is embedded as `nats.jwt` in the\n * authorization response.\n */\nexport function mintUserJwt(input: MintUserJwtInput): string {\n const accountKey = nkeys.fromSeed(Buffer.from(input.accountSeed)) as unknown as NkeyPair;\n const accountPublicKey = accountKey.getPublicKey();\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n const natsBlock: Record<string, unknown> = {\n type: USER_CLAIM_TYPE,\n version: NATS_CLAIM_VERSION,\n pub: input.permissions.pub,\n sub: input.permissions.sub,\n // Unlimited subscriptions / data / payload (the workspace subject scope, NOT\n // a connection-resource quota, is the boundary here).\n subs: -1,\n data: -1,\n payload: -1,\n };\n\n const claims: Record<string, unknown> = {\n jti: \"\",\n iat: nowSeconds,\n iss: accountPublicKey,\n name: input.name,\n sub: input.userPublicKey,\n // SERVER-config-mode placement: nats-server reads the embedded user JWT's `aud`\n // as the target account NAME (the configured `auth_callout.account`). This is\n // how the authenticated user binds to that account; the workspace isolation is\n // then carried by the pub/sub permissions below.\n aud: input.audienceAccount,\n nats: natsBlock,\n };\n if (typeof input.expiresAtSeconds === \"number\") {\n claims.exp = input.expiresAtSeconds;\n }\n return encodeJwt(claims, accountKey);\n}\n\n/**\n * Input to mint the authorization RESPONSE JWT the responder publishes back on the\n * request's reply subject (ADR-26 §3).\n * - `userPublicKey` — the request's `user_nkey`; the response `sub`.\n * - `serverId` — the request's `nats.server_id.id` (the server's public key); the\n * response `aud`.\n * - `accountSeed` — the callout account signing seed; signs the response and is\n * its `iss` (public key). NEVER logged.\n * - `userJwt` — the embedded signed user JWT (omit on a denial).\n * - `error` — a human-readable denial message (omit on success). When present the\n * server denies the connection.\n */\nexport interface MintAuthResponseInput {\n userPublicKey: string;\n serverId: string;\n accountSeed: string;\n userJwt?: string;\n error?: string;\n}\n\n/**\n * Mint the signed authorization-response JWT. On success it carries the embedded\n * user JWT (`nats.jwt`); on denial it carries `nats.error` and NO user JWT, which\n * makes nats-server refuse the connection. Signed by the callout account key (its\n * public key is `iss`); `sub` is the user_nkey, `aud` is the server id.\n */\nexport function mintAuthResponse(input: MintAuthResponseInput): string {\n const accountKey = nkeys.fromSeed(Buffer.from(input.accountSeed)) as unknown as NkeyPair;\n const accountPublicKey = accountKey.getPublicKey();\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n const natsBlock: Record<string, unknown> = {\n type: AUTH_RESPONSE_CLAIM_TYPE,\n version: NATS_CLAIM_VERSION,\n };\n if (input.userJwt) {\n natsBlock.jwt = input.userJwt;\n }\n if (input.error) {\n natsBlock.error = input.error;\n }\n\n const claims: Record<string, unknown> = {\n jti: \"\",\n iat: nowSeconds,\n iss: accountPublicKey,\n // The response `aud` MUST be the SERVER public key in server-config mode\n // (nats-server validates \"Audience must be a server public key\"). The\n // authenticated user is placed into the configured `auth_callout.account` (the\n // SAME account the responder + the privileged control plane connect into), so\n // `agent.<ws>.<id>.rpc` request/reply routes; the workspace isolation is carried\n // entirely by the user JWT's pub/sub subject permissions (NOT by cross-account\n // placement, which server-config-mode nats does not support — nats-io#4335).\n aud: input.serverId,\n sub: input.userPublicKey,\n nats: natsBlock,\n };\n return encodeJwt(claims, accountKey);\n}\n\n/**\n * The fields the responder needs out of the authorization REQUEST JWT (ADR-26 §2).\n * The request is itself a NATS JWT (`header.payload.signature`) the server signs;\n * we only DECODE it (the server proves its own identity by the connection, and the\n * embedded `auth_token` is independently HMAC-verified), so we read the payload\n * without re-verifying the server signature.\n */\nexport interface DecodedAuthRequest {\n /** The public user nkey the response user JWT MUST be `sub`-scoped to. */\n userNkey: string;\n /** The server's public id — the response `aud`. */\n serverId: string;\n /** The connect `auth_token` the client presented (our `oge_` bearer), if any. */\n authToken: string | undefined;\n /** The connect username, if any (unused today; present for completeness). */\n user: string | undefined;\n}\n\n/**\n * Decode the authorization-request JWT payload (the middle base64url segment). The\n * request shape (ADR-26 §2): `nats.user_nkey`, `nats.server_id.id`, and the\n * presented connect options under `nats.connect_opts` (`auth_token` / `user`).\n * Returns null on a malformed token so the caller can deny cleanly.\n */\nexport function decodeAuthRequest(token: string): DecodedAuthRequest | null {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(Buffer.from(parts[1]!, \"base64url\").toString(\"utf8\"));\n } catch {\n return null;\n }\n if (typeof payload !== \"object\" || payload === null) {\n return null;\n }\n const nats = (payload as { nats?: unknown }).nats;\n if (typeof nats !== \"object\" || nats === null) {\n return null;\n }\n const natsObj = nats as {\n user_nkey?: unknown;\n server_id?: { id?: unknown } | unknown;\n connect_opts?: { auth_token?: unknown; user?: unknown } | unknown;\n };\n const userNkey = typeof natsObj.user_nkey === \"string\" ? natsObj.user_nkey : null;\n if (!userNkey) {\n return null;\n }\n const serverIdRaw =\n typeof natsObj.server_id === \"object\" && natsObj.server_id !== null\n ? (natsObj.server_id as { id?: unknown }).id\n : undefined;\n const serverId = typeof serverIdRaw === \"string\" ? serverIdRaw : \"\";\n const connectOpts =\n typeof natsObj.connect_opts === \"object\" && natsObj.connect_opts !== null\n ? (natsObj.connect_opts as { auth_token?: unknown; user?: unknown })\n : {};\n const authToken = typeof connectOpts.auth_token === \"string\" ? connectOpts.auth_token : undefined;\n const user = typeof connectOpts.user === \"string\" ? connectOpts.user : undefined;\n return { userNkey, serverId, authToken, user };\n}\n\n/**\n * Build the workspace-scoped permission set for an agent: it may publish + subscribe\n * ONLY `agent.<workspaceId>.>` (its own RPC/event/hello subtree) and the reply\n * `_INBOX.>` subtree (so request/reply round-trips work). Everything else is\n * implicitly denied (an allow-list with no other entries IS the deny-all-else).\n *\n * THE isolation assertion (§17): with `workspaceId=A`, the returned allow lists name\n * only `agent.A.>` — so a connection bearing this credential is rejected by\n * nats-server the instant it tries to pub/sub `agent.B.>`. This is the per-workspace\n * tenancy boundary, enforced cryptographically by the signed JWT, not by naming.\n */\nexport function workspaceAgentPermissions(workspaceId: string): NatsPermissions {\n const agentScope = `agent.${workspaceId}.>`;\n // The reply-inbox subtree must be reachable for request/reply (the control plane\n // requests on agent.<ws>.<id>.rpc with a reply inbox; the agent responds there).\n const inboxScope = \"_INBOX.>\";\n return {\n pub: { allow: [agentScope, inboxScope] },\n sub: { allow: [agentScope, inboxScope] },\n };\n}\n"],"mappings":";AACA,SAAS,qBAAqB,sBAA4D;AAC1F,SAAS,SAAS,iBAA2F;;;ACA7G,IAAM,0BAA0B,oBAAI,IAAI;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AACF,CAAC;AASM,SAAS,2BAA2B,QAAwC;AACjF,QAAM,YAA4B,CAAC;AACnC,MAAI,MAAuB;AAE3B,QAAM,QAAQ,MAAM;AAClB,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AACA,cAAU,KAAK;AAAA,MACb,GAAG,IAAI;AAAA,MACP,SAAS;AAAA,QACP,MAAM,IAAI;AAAA,QACV,gBAAgB,IAAI;AAAA,QACpB,GAAI,IAAI,MAAM,SAAS,kCAAkC,IAAI,gBAAgB,SACzE,EAAE,MAAM,IAAI,YAAY,IACxB,CAAC;AAAA,MACP;AAAA,IACF,CAAC;AACD,UAAM;AAAA,EACR;AAEA,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,mBAAmB,KAAK,GAAG;AAC9B,YAAM;AACN,gBAAU,KAAK,KAAK;AACpB;AAAA,IACF;AAEA,UAAM,cAAc,MAAM,SAAS,iCAAiC,iBAAiB,MAAM,OAAO,IAAI;AACtG,QAAI,OAAO,aAAa,IAAI,OAAO,OAAO,IAAI,aAAa,WAAW,GAAG;AACvE,UAAI,QAAQ,UAAU,KAAK;AAC3B,UAAI,eAAe,MAAM;AACzB;AAAA,IACF;AAEA,UAAM;AACN,UAAM;AAAA,MACJ,OAAO;AAAA,MACP,cAAc,MAAM;AAAA,MACpB,MAAM,UAAU,KAAK;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAEA,QAAM;AACN,SAAO;AACT;AAEA,SAAS,mBAAmB,OAA8B;AACxD,SAAO,wBAAwB,IAAI,MAAM,IAAI;AAC/C;AAEA,SAAS,aACP,OACA,MACA,kBACA,iBACS;AACT,MAAI,MAAM,SAAS,KAAK,MAAM;AAC5B,WAAO;AAAA,EACT;AACA,OAAK,MAAM,UAAU,WAAW,KAAK,UAAU,OAAO;AACpD,WAAO;AAAA,EACT;AACA,SAAO,MAAM,SAAS,kCAAkC,qBAAqB;AAC/E;AAEA,SAAS,UAAU,OAA6B;AAC9C,MAAI,MAAM,SAAS,yBAAyB;AAC1C,WAAO,cAAc,MAAM,OAAO;AAAA,EACpC;AACA,QAAM,UAAU,SAAS,MAAM,OAAO;AACtC,MAAI,MAAM,SAAS,gCAAgC;AACjD,WAAO,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO,OAAO,QAAQ,WAAW,WAAW,QAAQ,SAAS;AAAA,EACjH;AACA,SAAO,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AAC3D;AAEA,SAAS,cAAc,SAA0B;AAC/C,QAAM,SAAS,SAAS,OAAO;AAC/B,MAAI,OAAO,OAAO,SAAS,UAAU;AACnC,WAAO,OAAO;AAAA,EAChB;AACA,QAAM,UAAU,SAAS,SAAS,OAAO,IAAI,EAAE,OAAO,EAAE;AACxD,MAAI,CAAC,MAAM,QAAQ,OAAO,GAAG;AAC3B,WAAO;AAAA,EACT;AACA,SAAO,QACJ,IAAI,CAAC,SAAS;AACb,UAAM,OAAO,SAAS,IAAI,EAAE;AAC5B,WAAO,OAAO,SAAS,WAAW,OAAO;AAAA,EAC3C,CAAC,EACA,KAAK,EAAE;AACZ;AAEA,SAAS,iBAAiB,SAAsC;AAC9D,QAAM,OAAO,SAAS,OAAO,EAAE;AAC/B,SAAO,OAAO,SAAS,WAAW,OAAO;AAC3C;AAEA,SAAS,SAAS,OAAyC;AACzD,SAAO,SAAS,OAAO,UAAU,WAAW,QAAmC,CAAC;AAClF;;;ACtFA,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AAItB,IAAM,aAAa,EAAE,KAAK,OAAO,KAAK,eAAe;AAGrD,IAAM,kBAAkB;AACxB,IAAM,2BAA2B;AACjC,IAAM,qBAAqB;AA2B3B,SAAS,gBAAgB,OAA2B;AAClD,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,WAAW;AAChD;AAIA,IAAM,kBAAkB;AACxB,SAAS,gBAAgB,OAA2B;AAClD,MAAI,OAAO;AACX,MAAI,QAAQ;AACZ,MAAI,MAAM;AACV,aAAW,QAAQ,OAAO;AACxB,YAAS,SAAS,IAAK;AACvB,YAAQ;AACR,WAAO,QAAQ,GAAG;AAChB,cAAQ;AACR,aAAO,gBAAiB,UAAU,OAAQ,EAAE;AAAA,IAC9C;AAAA,EACF;AACA,MAAI,OAAO,GAAG;AACZ,WAAO,gBAAiB,SAAU,IAAI,OAAS,EAAE;AAAA,EACnD;AACA,SAAO;AACT;AAQA,SAAS,WAAW,oBAAoC;AACtD,QAAM,OAAO,KAAK,UAAU,kBAAkB;AAC9C,QAAM,SAAS,WAAW,YAAY,EAAE,OAAO,MAAM,MAAM,EAAE,OAAO;AACpE,SAAO,gBAAgB,MAAM;AAC/B;AAQA,SAAS,UAAU,QAAiC,YAA8B;AAEhF,QAAM,eAAe,EAAE,GAAG,QAAQ,KAAK,GAAG;AAC1C,QAAM,MAAM,WAAW,YAAY;AACnC,QAAM,cAAc,EAAE,GAAG,QAAQ,IAAI;AAErC,QAAM,SAAS,gBAAgB,OAAO,KAAK,KAAK,UAAU,UAAU,GAAG,MAAM,CAAC;AAC9E,QAAM,UAAU,gBAAgB,OAAO,KAAK,KAAK,UAAU,WAAW,GAAG,MAAM,CAAC;AAChF,QAAM,eAAe,GAAG,MAAM,IAAI,OAAO;AACzC,QAAM,YAAY,WAAW,KAAK,OAAO,KAAK,cAAc,MAAM,CAAC;AACnE,SAAO,GAAG,YAAY,IAAI,gBAAgB,SAAS,CAAC;AACtD;AA+BO,SAAS,YAAY,OAAiC;AAC3D,QAAM,aAAa,MAAM,SAAS,OAAO,KAAK,MAAM,WAAW,CAAC;AAChE,QAAM,mBAAmB,WAAW,aAAa;AACjD,QAAM,aAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAE/C,QAAM,YAAqC;AAAA,IACzC,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK,MAAM,YAAY;AAAA,IACvB,KAAK,MAAM,YAAY;AAAA;AAAA;AAAA,IAGvB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AAEA,QAAM,SAAkC;AAAA,IACtC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,KAAK,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,IAKX,KAAK,MAAM;AAAA,IACX,MAAM;AAAA,EACR;AACA,MAAI,OAAO,MAAM,qBAAqB,UAAU;AAC9C,WAAO,MAAM,MAAM;AAAA,EACrB;AACA,SAAO,UAAU,QAAQ,UAAU;AACrC;AA4BO,SAAS,iBAAiB,OAAsC;AACrE,QAAM,aAAa,MAAM,SAAS,OAAO,KAAK,MAAM,WAAW,CAAC;AAChE,QAAM,mBAAmB,WAAW,aAAa;AACjD,QAAM,aAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAE/C,QAAM,YAAqC;AAAA,IACzC,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AACA,MAAI,MAAM,SAAS;AACjB,cAAU,MAAM,MAAM;AAAA,EACxB;AACA,MAAI,MAAM,OAAO;AACf,cAAU,QAAQ,MAAM;AAAA,EAC1B;AAEA,QAAM,SAAkC;AAAA,IACtC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQL,KAAK,MAAM;AAAA,IACX,KAAK,MAAM;AAAA,IACX,MAAM;AAAA,EACR;AACA,SAAO,UAAU,QAAQ,UAAU;AACrC;AA0BO,SAAS,kBAAkB,OAA0C;AAC1E,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO;AAAA,EACT;AACA,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,OAAO,KAAK,MAAM,CAAC,GAAI,WAAW,EAAE,SAAS,MAAM,CAAC;AAAA,EAC3E,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,WAAO;AAAA,EACT;AACA,QAAM,OAAQ,QAA+B;AAC7C,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,WAAO;AAAA,EACT;AACA,QAAM,UAAU;AAKhB,QAAM,WAAW,OAAO,QAAQ,cAAc,WAAW,QAAQ,YAAY;AAC7E,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AACA,QAAM,cACJ,OAAO,QAAQ,cAAc,YAAY,QAAQ,cAAc,OAC1D,QAAQ,UAA+B,KACxC;AACN,QAAM,WAAW,OAAO,gBAAgB,WAAW,cAAc;AACjE,QAAM,cACJ,OAAO,QAAQ,iBAAiB,YAAY,QAAQ,iBAAiB,OAChE,QAAQ,eACT,CAAC;AACP,QAAM,YAAY,OAAO,YAAY,eAAe,WAAW,YAAY,aAAa;AACxF,QAAM,OAAO,OAAO,YAAY,SAAS,WAAW,YAAY,OAAO;AACvE,SAAO,EAAE,UAAU,UAAU,WAAW,KAAK;AAC/C;AAaO,SAAS,0BAA0B,aAAsC;AAC9E,QAAM,aAAa,SAAS,WAAW;AAGvC,QAAM,aAAa;AACnB,SAAO;AAAA,IACL,KAAK,EAAE,OAAO,CAAC,YAAY,UAAU,EAAE;AAAA,IACvC,KAAK,EAAE,OAAO,CAAC,YAAY,UAAU,EAAE;AAAA,EACzC;AACF;;;AF5NA,SAAS,WAAAA,UAAS,SAAAC,cAAkC;AA9GpD,IAAM,QAAQ,UAA4C;AA0B1D,IAAM,oBAAoB;AAAA,EACxB,WAAW;AAAA,EACX,sBAAsB;AAAA,EACtB,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,YAAY;AACd;AAQA,SAAS,sBAAsB,SAA+C;AAC5E,SAAO,EAAE,GAAG,mBAAmB,GAAG,QAAQ;AAC5C;AAGA,IAAM,2BAA2B;AAWjC,eAAe,iBAAiB,IAAoB,WAAkC;AACpF,MAAI;AACJ,QAAM,UAAU,IAAI,QAAc,CAAC,YAAY;AAC7C,YAAQ,WAAW,SAAS,SAAS;AAAA,EACvC,CAAC;AACD,MAAI;AACF,UAAM,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,MAAM,MAAS,GAAG,OAAO,CAAC;AAAA,EACjE,UAAE;AACA,QAAI,OAAO;AACT,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AACF;AASA,SAAS,oBAAoB,IAAoB,OAAqB;AACpE,QAAM,YAAY;AAChB,QAAI;AACF,uBAAiB,UAAU,GAAG,OAAO,GAAG;AACtC,gBAAQ,KAAK,SAAS,KAAK,KAAK,OAAO,IAAI,IAAI,OAAO,IAAI;AAAA,MAC5D;AAAA,IACF,QAAQ;AAAA,IAGR;AAAA,EACF,GAAG;AACL;AAkGA,eAAsB,mBACpB,SACA,MACmB;AACnB,QAAM,iBAAoC,EAAE,SAAS,QAAQ;AAC7D,MAAI,MAAM;AACR,mBAAe,OAAO,KAAK;AAC3B,mBAAe,OAAO,KAAK;AAAA,EAC7B;AACA,QAAM,KAAK,MAAM,QAAQ,sBAAsB,cAAc,CAAC;AAC9D,sBAAoB,IAAI,WAAW;AACnC,QAAM,oBAAuC;AAAA,IAC3C,SAAS,OAAO,SAAS,SAAS,SAAS,aAAa,IAAI,SAAS,SAAS,KAAK,OAAO;AAAA,EAC5F;AACA,SAAO;AAAA,IACL,SAAS,OAAO,aAAa,WAAW,WAAW;AACjD,UAAI,OAAO,WAAW,GAAG;AACvB;AAAA,MACF;AASA,UAAI;AACF,WAAG,QAAQ,eAAe,aAAa,SAAS,GAAG,MAAM,OAAO,EAAE,aAAa,WAAW,OAAO,CAAC,CAAC;AAAA,MACrG,SAAS,OAAO;AAGd,gBAAQ;AAAA,UACN,6CAA6C,WAAW,IAAI,SAAS;AAAA,UACrE;AAAA,QACF;AACA;AAAA,MACF;AACA,YAAM,iBAAiB,IAAI,wBAAwB;AAAA,IACrD;AAAA,IACA,WAAW,OAAO,aAAa,WAAW,aAAa,iBAAiB,IAAI,aAAa,WAAW,QAAQ;AAAA,IAC5G,SAAS,OAAO,SAAS,SAAS,SAAS,aAAa,IAAI,SAAS,SAAS,KAAK,SAAS;AAAA,IAC5F,mBAAmB,CAAC,SAAS,YAAY,kBAAkB,IAAI,SAAS,OAAO;AAAA,IAC/E,sBAAsB,CAAC,SAAS,YAAY,qBAAqB,IAAI,SAAS,OAAO;AAAA,IACrF,sBAAsB,MAAM;AAAA,IAC5B,OAAO,YAAY;AACjB,YAAM,GAAG,MAAM;AAAA,IACjB;AAAA,EACF;AACF;AAmCA,eAAsB,0BACpB,SACA,MACA,SACA,SACA,UAA6B,CAAC,GACA;AAC9B,QAAM,iBAAoC,EAAE,SAAS,QAAQ;AAC7D,MAAI,QAAQ,MAAM;AAChB,mBAAe,OAAO,QAAQ;AAAA,EAChC;AACA,MAAI,KAAK,SAAS,iBAAiB;AACjC,mBAAe,OAAO,KAAK;AAC3B,mBAAe,OAAO,KAAK;AAAA,EAC7B,WAAW,KAAK,SAAS,SAAS;AAChC,mBAAe,QAAQ,KAAK;AAAA,EAC9B;AACA,QAAM,KAAK,MAAM,QAAQ,sBAAsB,cAAc,CAAC;AAC9D,sBAAoB,IAAI,QAAQ,OAAO,gBAAgB,QAAQ,IAAI,KAAK,cAAc;AACtF,QAAM,MAAoB,GAAG,UAAU,OAAO;AAC9C,QAAM,YAAY;AAChB,qBAAiB,OAAO,KAAK;AAC3B,UAAI,CAAC,IAAI,OAAO;AACd;AAAA,MACF;AACA,UAAI;AACF,cAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI,OAAO;AACjD,YAAI,QAAQ,KAAK;AAAA,MACnB,QAAQ;AAAA,MAGR;AAAA,IACF;AAAA,EACF,GAAG;AACH,SAAO;AAAA,IACL,OAAO,YAAY;AACjB,UAAI,YAAY;AAChB,YAAM,GAAG,MAAM;AAAA,IACjB;AAAA,EACF;AACF;AAEA,eAAsB,uBAAuB,IAAc,KAAe,aAAqB,WAAmB,QAAqD;AACrK,QAAM,WAAW,MAAM,oBAAoB,IAAI,aAAa,WAAW,MAAM;AAQ7E,MAAI;AACF,UAAM,IAAI,QAAQ,aAAa,WAAW,QAAQ;AAAA,EACpD,SAAS,OAAO;AACd,YAAQ;AAAA,MACN,oCAAoC,WAAW,IAAI,SAAS,KAAK,SAAS,MAAM;AAAA,MAChF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,IAAoB,aAAqB,WAAmB,UAAwE;AAC5J,QAAM,MAAoB,GAAG,UAAU,eAAe,aAAa,SAAS,CAAC;AAC7E,QAAM,YAAY;AAChB,qBAAiB,OAAO,KAAK;AAC3B,YAAM,UAAU,MAAM,OAAO,IAAI,IAAI;AACrC,YAAM,SAAS,YAAY,UAAU,QAAQ,SAAS,CAAC,OAAO;AAC9D,YAAM,SAAS,MAAM;AAAA,IACvB;AAAA,EACF,GAAG;AACH,SAAO,MAAM;AACX,QAAI,YAAY;AAAA,EAClB;AACF;AASA,eAAe,aAAa,IAAoB,SAAiB,SAAqB,SAAwC;AAC5H,QAAM,MAAW,MAAM,GAAG,QAAQ,SAAS,SAAS,EAAE,QAAQ,CAAC;AAC/D,SAAO,EAAE,MAAM,IAAI,KAAK;AAC1B;AASA,SAAS,kBAAkB,IAAoB,SAAiB,SAAqC;AACnG,QAAM,MAAoB,GAAG,UAAU,OAAO;AAC9C,QAAM,YAAY;AAChB,qBAAiB,OAAO,KAAK;AAG3B,UAAI,CAAC,IAAI,OAAO;AACd;AAAA,MACF;AACA,UAAI;AACF,cAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI,OAAO;AACjD,YAAI,QAAQ,KAAK;AAAA,MACnB,QAAQ;AAAA,MAIR;AAAA,IACF;AAAA,EACF,GAAG;AACH,SAAO,MAAM;AACX,QAAI,YAAY;AAAA,EAClB;AACF;AASA,SAAS,qBACP,IACA,SACA,SACY;AACZ,QAAM,MAAoB,GAAG,UAAU,OAAO;AAC9C,QAAM,YAAY;AAChB,qBAAiB,OAAO,KAAK;AAC3B,UAAI;AACF,cAAM,QAAQ,IAAI,MAAM,IAAI,OAAO;AAAA,MACrC,QAAQ;AAAA,MAGR;AAAA,IACF;AAAA,EACF,GAAG;AACH,SAAO,MAAM;AACX,QAAI,YAAY;AAAA,EAClB;AACF;AAEO,SAAS,UAAU,OAA6B;AACrD,SAAO;AAAA,IACL,OAAO,MAAM,QAAQ;AAAA,IACrB,UAAU,MAAM,IAAI;AAAA,IACpB,SAAS,KAAK,UAAU,KAAK,CAAC;AAAA,IAC9B;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;","names":["connect","nkeys"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/coalesce.ts","../src/nats-jwt.ts"],"sourcesContent":["import type { SessionBusMessage, SessionEvent } from \"@opengeni/contracts\";\nimport { appendSessionEvents, sessionSubject, type AppendEventInput, type Database } from \"@opengeni/db\";\nimport { connect, JSONCodec, type ConnectionOptions, type Msg, type NatsConnection, type Subscription } from \"nats\";\n\nconst codec = JSONCodec<SessionBusMessage | SessionEvent>();\n\nexport type EventLogger = {\n debug?: (message: string, attributes?: Record<string, unknown>) => void;\n warn?: (message: string, attributes?: Record<string, unknown>) => void;\n};\n\nexport type EventBusOptions = {\n logger?: EventLogger;\n};\n\nconst silentLogger: Required<EventLogger> = {\n debug: () => {},\n warn: () => {},\n};\n\nexport { coalesceSessionEventDeltas } from \"./coalesce\";\n\n/**\n * Reconnect + keepalive defaults applied to EVERY long-lived NATS connection\n * this package opens (the event bus AND the standalone auth-callout responder).\n *\n * The production outage these guard against: an in-cluster NATS broker pod\n * restart. nats.js's stock policy gives up after ~10 attempts (~20s) and the\n * client goes permanently CONNECTION_CLOSED — which takes the whole control\n * plane down with it: every session-create publishes events to NATS, and the\n * API-hosted auth-callout responder dies so BYO agents get \"authorization\n * violation\". Recovery then required a MANUAL api+worker restart. With these\n * options the client retries forever and auto-recovers the moment the broker\n * returns. Factored into one source of truth so the call sites never drift.\n *\n * - `reconnect` + `maxReconnectAttempts: -1` — never give up (infinite retry).\n * - `reconnectTimeWait` (2s base) + `reconnectJitter`/`reconnectJitterTLS`\n * (up to 1s) — a fleet of api/worker pods doesn't thundering-herd the broker\n * on recovery.\n * - `waitOnFirstConnect` — a broker briefly unavailable at boot must not\n * hard-fail the process; the client keeps trying instead of throwing.\n * - `pingInterval`/`maxPingOut` — promptly detect a silently-dead socket so the\n * reconnect machinery actually engages instead of hanging on a zombie.\n */\nconst RECONNECT_OPTIONS = {\n reconnect: true,\n maxReconnectAttempts: -1,\n reconnectTimeWait: 2_000,\n reconnectJitter: 1_000,\n reconnectJitterTLS: 1_000,\n waitOnFirstConnect: true,\n pingInterval: 20_000,\n maxPingOut: 3,\n} satisfies ConnectionOptions;\n\n/**\n * The single source of truth for a long-lived connection's resilience: merge the\n * reconnect/keepalive defaults UNDER the caller's connection options (servers +\n * optional auth/name). Every long-lived `connect()` in this package goes through\n * here so the two call sites can never diverge.\n */\nfunction withReconnectDefaults(options: ConnectionOptions): ConnectionOptions {\n return { ...RECONNECT_OPTIONS, ...options };\n}\n\n/** How long a best-effort publish waits on `flush()` before giving up (see `publish`). */\nconst PUBLISH_FLUSH_TIMEOUT_MS = 2_000;\n\n/**\n * Await `nc.flush()` but never longer than `timeoutMs`. With infinite reconnect a\n * `flush()` issued while the broker is down does NOT reject — it pends until the\n * broker returns, which can be minutes. Racing it against a timer keeps a long\n * outage from stalling an in-flight turn; the published message stays buffered\n * and is delivered on reconnect regardless. A flush rejection (connection fully\n * CLOSED) is swallowed here so the timeout race never leaks an unhandled\n * rejection — the caller's publish path is what logs the drop.\n */\nasync function flushWithTimeout(nc: NatsConnection, timeoutMs: number): Promise<void> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs);\n });\n try {\n await Promise.race([nc.flush().catch(() => undefined), timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\n/**\n * Drain a long-lived connection's status async-iterator to the log so a future\n * broker outage is OBSERVABLE (disconnect → reconnecting → reconnect → update).\n * Fire-and-forget for the connection's lifetime; the loop ends when the\n * connection closes. `label` distinguishes the event-bus connection from the\n * auth-callout responder in the logs.\n */\nfunction logConnectionStatus(\n nc: NatsConnection,\n label: string,\n logger: EventLogger = silentLogger,\n onStatus?: (type: string) => void,\n): void {\n void (async () => {\n try {\n for await (const status of nc.status()) {\n onStatus?.(status.type);\n const attributes = { label, status: status.type, data: status.data };\n if (isWarnNatsStatus(status.type)) {\n (logger.warn ?? silentLogger.warn)(\"NATS connection status\", attributes);\n } else {\n (logger.debug ?? silentLogger.debug)(\"NATS connection status\", attributes);\n }\n }\n } catch {\n // The status iterator simply ends when the connection closes; never let it\n // throw out of this background loop.\n }\n })();\n}\n\nfunction isWarnNatsStatus(type: string): boolean {\n return type === \"disconnect\" || type === \"error\" || type === \"staleConnection\";\n}\n\nexport {\n decodeAuthRequest,\n mintAuthResponse,\n mintUserJwt,\n workspaceAgentPermissions,\n type DecodedAuthRequest,\n type MintAuthResponseInput,\n type MintUserJwtInput,\n type NatsPermission,\n type NatsPermissions,\n} from \"./nats-jwt\";\n\n// Re-export the raw NATS primitives a consumer needs to open a direct connection or\n// generate nkeys (the auth-callout responder's standalone connection, the\n// agent-simulating integration tests). This keeps `nats` an internal dependency of\n// this leaf — callers in the bun workspace reach it through @opengeni/events rather\n// than depending on `nats` directly.\nexport { connect, nkeys, type NatsConnection } from \"nats\";\n\n/**\n * A raw request/reply reply — just the response bytes. Mirrors the subset of the\n * NATS `Msg` shape a binary request/reply caller needs (`NatsControlRpc` consumes\n * exactly this). Kept minimal so the events package does not leak the `nats` `Msg`\n * type into the agent-loop-free runtime leaf.\n */\nexport type RequestReply = { data: Uint8Array };\n\n/**\n * The minimal request/reply connection the selfhosted control plane consumes\n * (structurally identical to `@opengeni/runtime`'s `NatsRequestConnection`). The\n * API/worker hand this accessor to `NatsControlRpc` so the control transport rides\n * the SAME managed NATS connection the event bus already owns — a NATS connection\n * natively supports both pub/sub and request/reply, so there is NEVER a second\n * connection.\n */\nexport interface RequestConnection {\n request(subject: string, payload: Uint8Array, opts: { timeout: number }): Promise<RequestReply>;\n}\n\n/**\n * A handler answering a request/reply on a subscribed subject: given the request\n * bytes (+ the concrete subject the message landed on, for `agent.<ws>.<id>.rpc`\n * style wildcard routing), return the response bytes to reply with. A thrown error\n * leaves the request unanswered (the caller's request times out / sees no\n * responder), which the control plane maps to `agent_offline` / reconnecting.\n */\nexport type RequestHandler = (request: Uint8Array, subject: string) => Promise<Uint8Array> | Uint8Array;\n\nexport type EventBus = {\n publish: (workspaceId: string, sessionId: string, events: SessionEvent[]) => Promise<void>;\n subscribe: (workspaceId: string, sessionId: string, onEvents: (events: SessionEvent[]) => void | Promise<void>) => Promise<() => void>;\n /**\n * Issue a binary request/reply on a subject over the bus's NATS connection\n * (the selfhosted control plane: `agent.<ws>.<id>.rpc`). A new usage of what was\n * a one-way bus — same connection, native NATS request/reply. Rejects on a\n * no-responder (NATS 503) or a request timeout; the caller (`NatsControlRpc`)\n * maps those to `agent_offline` / `agent_reconnecting`, never a NotFound.\n */\n request: (subject: string, payload: Uint8Array, opts: { timeoutMs: number }) => Promise<RequestReply>;\n /**\n * Subscribe-and-reply on a subject (the responder side — the enrolled agent, or\n * a test stand-in for it): for every request on `subject`, call `handler` and\n * `respond` with its bytes over the SAME connection. Returns an unsubscribe fn.\n * A subject may be a NATS wildcard (e.g. `agent.*.*.rpc`).\n */\n subscribeRequests: (subject: string, handler: RequestHandler) => () => void;\n /**\n * Subscribe to the agent EVENT plane (the one-way fire-and-forget heartbeats +\n * going-offline the agent PUBLISHES on `agent.<ws>.<id>.events`, NOT a\n * request/reply). The M10 metrics-ingestion consumer subscribes the wildcard\n * `agent.*.*.events` and gets each raw payload plus its concrete subject (so it\n * can extract `<ws>`/`<id>` for the per-enrollment upsert). Returns an\n * unsubscribe fn. Decoding the AgentEvent is the caller's concern (this leaf\n * does not depend on `@opengeni/agent-proto`).\n */\n subscribeAgentEvents: (\n subject: string,\n handler: (payload: Uint8Array, subject: string) => void | Promise<void>,\n ) => () => void;\n /**\n * The `RequestConnection` accessor the selfhosted `NatsControlRpc` consumes —\n * the SAME managed connection (pub/sub + request/reply share it). The control\n * plane injects this so the transport never opens a second connection.\n */\n getRequestConnection: () => RequestConnection;\n isConnected?: () => boolean;\n close: () => Promise<void>;\n};\n\n/**\n * Connect the event bus + control-plane request/reply over ONE managed NATS\n * connection. `auth` is the PRIVILEGED control-plane login (M-AUTH): when the\n * server runs with auth_callout, the api/worker authenticates as a static account\n * user permitted to request `agent.*.rpc` + receive its inbox replies. When `auth`\n * is omitted the connection is anonymous (local dev / a NATS without auth_callout)\n * — the existing behavior, unchanged.\n */\nexport async function createNatsEventBus(\n natsUrl: string,\n auth?: { user: string; pass: string },\n options: EventBusOptions = {},\n): Promise<EventBus> {\n const connectOptions: ConnectionOptions = { servers: natsUrl };\n if (auth) {\n connectOptions.user = auth.user;\n connectOptions.pass = auth.pass;\n }\n const nc = await connect(withReconnectDefaults(connectOptions));\n let connected = true;\n logConnectionStatus(nc, \"event-bus\", options.logger, (type) => {\n if (type === \"disconnect\" || type === \"reconnecting\" || type === \"staleConnection\" || type === \"error\") {\n connected = false;\n } else if (type === \"connect\" || type === \"reconnect\") {\n connected = true;\n }\n });\n const requestConnection: RequestConnection = {\n request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeout),\n };\n return {\n publish: async (workspaceId, sessionId, events) => {\n if (events.length === 0) {\n return;\n }\n // Best-effort LIVE fan-out. These events are ALREADY durably appended to\n // the DB before we get here (they carry a DB-assigned `sequence`), and\n // every consumer reconciles from that durable log — the server SSE stream\n // replays + gap-backfills via `listSessionEvents`, and the SDK client\n // reconnects and replays from the durable events endpoint. So a publish\n // that fails during a broker blip only delays LIVE delivery (healed by the\n // next successful publish's gap-backfill, or a stream reconnect); it must\n // never throw the in-flight turn to death.\n try {\n nc.publish(sessionSubject(workspaceId, sessionId), codec.encode({ workspaceId, sessionId, events }));\n } catch (error) {\n // `publish()` throws synchronously only when the connection is fully\n // CLOSED (with infinite reconnect, effectively never outside shutdown).\n (options.logger?.warn ?? silentLogger.warn)(\"NATS live publish dropped; events are durable in the DB and reconcile on stream replay\", {\n workspaceId,\n sessionId,\n error: error instanceof Error ? error.message : String(error),\n });\n return;\n }\n await flushWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);\n },\n subscribe: async (workspaceId, sessionId, onEvents) => subscribeSession(nc, workspaceId, sessionId, onEvents),\n request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeoutMs),\n subscribeRequests: (subject, handler) => subscribeRequests(nc, subject, handler),\n subscribeAgentEvents: (subject, handler) => subscribeAgentEvents(nc, subject, handler),\n getRequestConnection: () => requestConnection,\n isConnected: () => connected && !nc.isClosed() && !nc.isDraining(),\n close: async () => {\n await nc.drain();\n },\n };\n}\n\n/**\n * A standalone NATS connection answering request/reply on ONE subject — the\n * transport primitive the auth-callout responder uses. It is DELIBERATELY a\n * SEPARATE connection from the event bus: the callout responder authenticates as\n * the callout account's `auth_users` user (a username/password or token in the\n * `AUTH` account), which is a DIFFERENT identity from the control-plane's\n * privileged account that the event bus + `NatsControlRpc` ride. One connection\n * per identity; never multiplex the two.\n *\n * `request`/`reply` here is the RAW NATS request/reply (`$SYS.REQ.USER.AUTH`): the\n * server publishes an authorization request with a reply inbox; the handler returns\n * the signed authorization-response bytes which we `respond` on that inbox.\n */\nexport interface ResponderConnection {\n /** Subscribe-and-reply on `subject`; returns an async close that drains. */\n close: () => Promise<void>;\n}\n\n/** Connection auth for a standalone NATS connection (the callout responder). */\nexport type NatsConnectAuth =\n | { kind: \"user-password\"; user: string; pass: string }\n | { kind: \"token\"; token: string }\n | { kind: \"anonymous\" };\n\n/**\n * Open a standalone NATS connection and subscribe `subject`, replying to every\n * request with `handler(requestBytes, subject)`. Used by the auth-callout\n * responder to serve `$SYS.REQ.USER.AUTH` as the callout auth user. Returns a\n * handle whose `close()` drains the connection. A handler that throws leaves the\n * request UNANSWERED — for auth-callout that means the server denies the\n * connection on its own timeout, which is the correct fail-closed behavior (a\n * responder bug must never accidentally grant access).\n */\nexport async function createResponderConnection(\n natsUrl: string,\n auth: NatsConnectAuth,\n subject: string,\n handler: RequestHandler,\n options: { name?: string; logger?: EventLogger } = {},\n): Promise<ResponderConnection> {\n const connectOptions: ConnectionOptions = { servers: natsUrl };\n if (options.name) {\n connectOptions.name = options.name;\n }\n if (auth.kind === \"user-password\") {\n connectOptions.user = auth.user;\n connectOptions.pass = auth.pass;\n } else if (auth.kind === \"token\") {\n connectOptions.token = auth.token;\n }\n const nc = await connect(withReconnectDefaults(connectOptions));\n logConnectionStatus(nc, options.name ? `auth-callout:${options.name}` : \"auth-callout\", options.logger);\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n if (!msg.reply) {\n continue;\n }\n try {\n const reply = await handler(msg.data, msg.subject);\n msg.respond(reply);\n } catch {\n // Leave UNANSWERED — fail-closed. The server denies the connect attempt\n // on its callout timeout; a responder error never grants access.\n }\n }\n })();\n return {\n close: async () => {\n sub.unsubscribe();\n await nc.drain();\n },\n };\n}\n\nexport async function appendAndPublishEvents(db: Database, bus: EventBus, workspaceId: string, sessionId: string, events: AppendEventInput[]): Promise<SessionEvent[]> {\n const appended = await appendSessionEvents(db, workspaceId, sessionId, events);\n // The DB append above is the durable system of record; the publish is only a\n // best-effort LIVE fan-out. Guard it so NO EventBus implementation can throw an\n // in-flight agent turn to death on a transient NATS disconnect — consumers\n // reconcile any missed live events from the durable log via the events/stream\n // endpoint (DB replay + gap-backfill). The managed `createNatsEventBus` bus\n // already swallows internally, so this catch is the belt-and-suspenders guard\n // for any other bus impl (and a fully CLOSED connection during shutdown).\n try {\n await bus.publish(workspaceId, sessionId, appended);\n } catch (error) {\n console.warn(\n `[events] live publish failed for ${workspaceId}/${sessionId}; ${appended.length} event(s) are durable and reconcile on stream replay`,\n error,\n );\n }\n return appended;\n}\n\nfunction subscribeSession(nc: NatsConnection, workspaceId: string, sessionId: string, onEvents: (events: SessionEvent[]) => void | Promise<void>): () => void {\n const sub: Subscription = nc.subscribe(sessionSubject(workspaceId, sessionId));\n void (async () => {\n for await (const msg of sub) {\n const decoded = codec.decode(msg.data) as SessionBusMessage | SessionEvent;\n const events = \"events\" in decoded ? decoded.events : [decoded];\n await onEvents(events);\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\n/**\n * A binary request/reply over the managed connection. Returns ONLY the reply\n * bytes (the `RequestReply` shape) — the request/reply error semantics (a\n * no-responder NATS 503, a request timeout) propagate as the rejected promise so\n * the caller owns the mapping. The reply is delivered via the connection's\n * built-in mux inbox; no extra subscription is created here.\n */\nasync function requestReply(nc: NatsConnection, subject: string, payload: Uint8Array, timeout: number): Promise<RequestReply> {\n const msg: Msg = await nc.request(subject, payload, { timeout });\n return { data: msg.data };\n}\n\n/**\n * Subscribe to `subject` and reply to every request with the handler's bytes,\n * over the SAME connection. The responder side of request/reply: each delivered\n * `Msg` carries a `reply` inbox; `msg.respond(bytes)` publishes the answer there.\n * A handler that throws (or a message with no `reply` subject) is left unanswered\n * — the requester then sees a timeout, never a malformed reply.\n */\nfunction subscribeRequests(nc: NatsConnection, subject: string, handler: RequestHandler): () => void {\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n // A request always carries a reply inbox; a plain publish to this subject\n // (no reply) is ignored — request/reply is the only contract here.\n if (!msg.reply) {\n continue;\n }\n try {\n const reply = await handler(msg.data, msg.subject);\n msg.respond(reply);\n } catch {\n // Leave the request unanswered: the requester's request times out, which\n // the selfhosted control plane reads as a transient blip (reconnecting),\n // never a malformed reply. The responder stays subscribed for the next op.\n }\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\n/**\n * Subscribe to the one-way agent event plane: deliver each published payload (the\n * agent's `AgentEvent` heartbeat / going-offline, NOT a request/reply) to the\n * handler with its concrete subject. A plain `nc.subscribe` (no reply); a handler\n * that throws is swallowed so one bad event never tears down the subscription\n * (ingestion is best-effort — a metrics gap is never fatal).\n */\nfunction subscribeAgentEvents(\n nc: NatsConnection,\n subject: string,\n handler: (payload: Uint8Array, subject: string) => void | Promise<void>,\n): () => void {\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n try {\n await handler(msg.data, msg.subject);\n } catch {\n // Swallow: best-effort ingestion. The subscription stays live for the\n // next event.\n }\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\nexport function formatSse(event: SessionEvent): string {\n return [\n `id: ${event.sequence}`,\n `event: ${event.type}`,\n `data: ${JSON.stringify(event)}`,\n \"\",\n \"\",\n ].join(\"\\n\");\n}\n","import type { SessionEvent } from \"@opengeni/contracts\";\n\nconst COALESCIBLE_DELTA_TYPES = new Set([\n \"agent.message.delta\",\n \"agent.reasoning.delta\",\n \"sandbox.command.output.delta\",\n]);\n\ntype DeltaRun = {\n first: SessionEvent;\n lastSequence: number;\n text: string;\n sandboxName: string | undefined;\n sandboxStream: string | undefined;\n sandboxCommandId: string | undefined;\n};\n\nexport function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent[] {\n const coalesced: SessionEvent[] = [];\n let run: DeltaRun | null = null;\n\n const flush = () => {\n if (!run) {\n return;\n }\n coalesced.push({\n ...run.first,\n payload: run.first.type === \"sandbox.command.output.delta\"\n // Sandbox output keeps its CANONICAL field (`chunk` — the terminal and\n // projection read it) plus the stream/commandId identity of the run.\n ? {\n chunk: run.text,\n coalescedUntil: run.lastSequence,\n ...(run.sandboxStream !== undefined ? { stream: run.sandboxStream } : {}),\n ...(run.sandboxCommandId !== undefined ? { commandId: run.sandboxCommandId } : {}),\n ...(run.sandboxName !== undefined ? { name: run.sandboxName } : {}),\n }\n : {\n text: run.text,\n coalescedUntil: run.lastSequence,\n },\n });\n run = null;\n };\n\n for (const event of events) {\n if (!isCoalescibleDelta(event)) {\n flush();\n coalesced.push(event);\n continue;\n }\n\n const isSandbox = event.type === \"sandbox.command.output.delta\";\n const sandboxName = isSandbox ? sandboxDeltaName(event.payload) : undefined;\n const sandboxStream = isSandbox ? sandboxDeltaString(event.payload, \"stream\") : undefined;\n const sandboxCommandId = isSandbox ? sandboxDeltaString(event.payload, \"commandId\") : undefined;\n if (\n run\n && sameDeltaRun(run.first, event, run.sandboxName, sandboxName)\n && run.sandboxStream === sandboxStream\n && run.sandboxCommandId === sandboxCommandId\n ) {\n run.text += deltaText(event);\n run.lastSequence = event.sequence;\n continue;\n }\n\n flush();\n run = {\n first: event,\n lastSequence: event.sequence,\n text: deltaText(event),\n sandboxName,\n sandboxStream,\n sandboxCommandId,\n };\n }\n\n flush();\n return coalesced;\n}\n\nfunction isCoalescibleDelta(event: SessionEvent): boolean {\n return COALESCIBLE_DELTA_TYPES.has(event.type);\n}\n\nfunction sameDeltaRun(\n first: SessionEvent,\n next: SessionEvent,\n firstSandboxName: string | undefined,\n nextSandboxName: string | undefined,\n): boolean {\n if (first.type !== next.type) {\n return false;\n }\n if ((first.turnId ?? null) !== (next.turnId ?? null)) {\n return false;\n }\n return first.type !== \"sandbox.command.output.delta\" || firstSandboxName === nextSandboxName;\n}\n\nfunction deltaText(event: SessionEvent): string {\n if (event.type === \"agent.reasoning.delta\") {\n return reasoningText(event.payload);\n }\n const payload = asRecord(event.payload);\n if (event.type === \"sandbox.command.output.delta\") {\n // `chunk` is the canonical wire field (contracts SandboxCommandOutputDeltaPayload);\n // text/output are tolerated legacy shapes.\n for (const key of [\"chunk\", \"text\", \"output\"] as const) {\n if (typeof payload[key] === \"string\") {\n return payload[key] as string;\n }\n }\n return \"\";\n }\n return typeof payload.text === \"string\" ? payload.text : \"\";\n}\n\nfunction reasoningText(payload: unknown): string {\n const record = asRecord(payload);\n if (typeof record.text === \"string\") {\n return record.text;\n }\n const content = asRecord(asRecord(record.item).rawItem).content;\n if (!Array.isArray(content)) {\n return \"\";\n }\n return content\n .map((part) => {\n const text = asRecord(part).text;\n return typeof text === \"string\" ? text : \"\";\n })\n .join(\"\");\n}\n\nfunction sandboxDeltaName(payload: unknown): string | undefined {\n const name = asRecord(payload).name;\n return typeof name === \"string\" ? name : undefined;\n}\n\nfunction sandboxDeltaString(payload: unknown, key: \"stream\" | \"commandId\"): string | undefined {\n const value = asRecord(payload)[key];\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" ? value as Record<string, unknown> : {};\n}\n","// packages/events/src/nats-jwt.ts — NATS JWT v2 signing for the auth-callout\n// responder (bring-your-own-compute M-AUTH; dossier §10.1 NATS Accounts per\n// workspace + §17 the isolation smoke).\n//\n// This is the cryptographic core of the auth-callout tenancy boundary. When an\n// external agent connects to NATS presenting its `oge_` enrollment bearer as the\n// connect auth-token, nats-server (configured with `auth_callout`) issues an\n// authorization request on `$SYS.REQ.USER.AUTH`. Our responder (auth-callout.ts)\n// validates the bearer and answers with a SIGNED authorization-response JWT that\n// embeds a SIGNED user JWT scoping the connection to publish/subscribe ONLY\n// `agent.<workspaceId>.>` (+ the reply `_INBOX.>`). That per-subject permission\n// set IS the per-workspace isolation: workspace A's agent literally cannot\n// pub/sub workspace B's subjects (§19 the NATS-Accounts-misconfig leak risk is\n// closed at the JWT-permission layer, not just by subject naming).\n//\n// WHY HAND-ROLL THE JWT ENCODING (vs a dep): the NATS JWT v2 wire format is small,\n// stable, and fully specified (ADR-26 + nats-io/jwt): a base64url header\n// `{\"typ\":\"JWT\",\"alg\":\"ed25519-nkey\"}`, base64url JSON claims whose `jti` is the\n// base32(SHA-512/256(claims-with-blank-jti)), and an ed25519 nkey signature over\n// `header.payload`. nkeys (re-exported by the `nats` package we already depend on)\n// gives us the ed25519 sign primitive; Node `crypto` gives SHA-512/256. So we own\n// the encoding in a few well-tested functions rather than pull an alpha\n// `@nats-io/jwt` (0.0.x) whose nkeys-version compat is uncertain. No `xkey`\n// encryption is used (the bearer is already an authenticated identity claim and\n// the wire is TLS — encryption is an optional ADR-26 hardening, off here).\n//\n// SECURITY: the account SIGNING SEED never leaves this process and is NEVER logged.\n// Callers pass it as a `string` seed; we `fromSeed` it once per sign. The bearer\n// the responder validates is HMAC-verified elsewhere (verifyEnrollmentBearer); this\n// module only mints the scoped NATS credential once identity is proven.\n\nimport { createHash } from \"node:crypto\";\nimport { nkeys } from \"nats\";\n\n/** The NATS JWT v2 header — constant for every token we mint (ADR-26 / nats-io/jwt:\n * `TokenTypeJwt=\"JWT\"`, `AlgorithmNkey=\"ed25519-nkey\"`). */\nconst JWT_HEADER = { typ: \"JWT\", alg: \"ed25519-nkey\" } as const;\n\n/** NATS user-claim `nats.type` discriminator + `nats.version` for v2 claims. */\nconst USER_CLAIM_TYPE = \"user\";\nconst AUTH_RESPONSE_CLAIM_TYPE = \"authorization_response\";\nconst NATS_CLAIM_VERSION = 2;\n\n/** A NATS permission set: subject allow/deny lists (ADR-26 `pub`/`sub` →\n * `allow`/`deny`). An empty/undefined list means \"no explicit grant\" — combined\n * with the agent scope below, the connection can ONLY reach what `allow` lists. */\nexport interface NatsPermission {\n allow?: string[];\n deny?: string[];\n}\n\n/** The pub/sub permissions embedded in a user JWT. */\nexport interface NatsPermissions {\n pub: NatsPermission;\n sub: NatsPermission;\n}\n\n/**\n * The minimal nkey keypair surface this module needs — exactly what\n * `nkeys.fromSeed(seed)` returns. Declared structurally so the module does not\n * leak the `nats` nkeys type through its public signature.\n */\ninterface NkeyPair {\n getPublicKey(): string;\n sign(input: Uint8Array): Uint8Array;\n}\n\n/** base64url (RawURLEncoding — no padding), matching nats-io/jwt's `serialize`. */\nfunction base64UrlEncode(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString(\"base64url\");\n}\n\n/** RFC 4648 base32 (standard alphabet, NO padding) — the encoding nats-io/jwt\n * uses for the `jti` hash. Node has no built-in base32, so a tiny encoder. */\nconst BASE32_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\nfunction base32NoPadding(bytes: Uint8Array): string {\n let bits = 0;\n let value = 0;\n let out = \"\";\n for (const byte of bytes) {\n value = (value << 8) | byte;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n out += BASE32_ALPHABET[(value >>> bits) & 31];\n }\n }\n if (bits > 0) {\n out += BASE32_ALPHABET[(value << (5 - bits)) & 31];\n }\n return out;\n}\n\n/**\n * Compute the canonical NATS `jti`: base32(NoPadding, std-alphabet) of the\n * SHA-512/256 of the claims object SERIALIZED WITH AN EMPTY `jti` (nats-io/jwt's\n * `hash`). nats-server recomputes + verifies this on decode, so it must match\n * byte-for-byte. We serialize the SAME object we will sign, only with `jti:\"\"`.\n */\nfunction computeJti(claimsWithBlankJti: object): string {\n const json = JSON.stringify(claimsWithBlankJti);\n const digest = createHash(\"sha512-256\").update(json, \"utf8\").digest();\n return base32NoPadding(digest);\n}\n\n/**\n * Encode + sign a NATS v2 JWT. The `claims` MUST already carry `iss`/`sub`/`iat`\n * (+ optional `aud`/`exp`) and a `nats` block; this function fills `jti` (the\n * canonical hash), serializes `header.payload`, signs that with `signingKey`, and\n * appends the base64url signature. Returns the compact `header.payload.signature`.\n */\nfunction encodeJwt(claims: Record<string, unknown>, signingKey: NkeyPair): string {\n // jti is the hash of the claims with jti blanked — set it blank, hash, then set.\n const withBlankJti = { ...claims, jti: \"\" };\n const jti = computeJti(withBlankJti);\n const finalClaims = { ...claims, jti };\n\n const header = base64UrlEncode(Buffer.from(JSON.stringify(JWT_HEADER), \"utf8\"));\n const payload = base64UrlEncode(Buffer.from(JSON.stringify(finalClaims), \"utf8\"));\n const signingInput = `${header}.${payload}`;\n const signature = signingKey.sign(Buffer.from(signingInput, \"utf8\"));\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\n/**\n * Input to mint a workspace-scoped NATS user JWT for an enrolled agent.\n * - `userPublicKey` — the `user_nkey` from the authorization request; it MUST be\n * the `sub` of the user JWT (nats-server rejects a mismatch).\n * - `accountSeed` — the callout account SIGNING seed (`SA...`); both the user JWT\n * `iss` (its public key) and the signature come from it. NEVER logged.\n * - `name` — a human label for the user (the agent id), for server logs.\n * - `permissions` — the pub/sub allow/deny lists (the workspace scope).\n * - `expiresAtSeconds` — optional absolute `exp` (unix seconds). When set the\n * server will expire the connection's credential; we tie it to the bearer's\n * remaining life so a revoked/expired enrollment cannot outlive its bearer.\n */\nexport interface MintUserJwtInput {\n userPublicKey: string;\n accountSeed: string;\n name: string;\n permissions: NatsPermissions;\n /** The target account NAME (the `auth_callout.account`) the user binds to; the\n * embedded user JWT's `aud` in server-config mode. */\n audienceAccount: string;\n expiresAtSeconds?: number;\n}\n\n/**\n * Mint a signed NATS user JWT scoped by `permissions`. In auth-callout SERVER\n * mode the user JWT is signed by the callout ISSUER ACCOUNT key, and its `iss` is\n * that account's public key. The returned JWT is embedded as `nats.jwt` in the\n * authorization response.\n */\nexport function mintUserJwt(input: MintUserJwtInput): string {\n const accountKey = nkeys.fromSeed(Buffer.from(input.accountSeed)) as unknown as NkeyPair;\n const accountPublicKey = accountKey.getPublicKey();\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n const natsBlock: Record<string, unknown> = {\n type: USER_CLAIM_TYPE,\n version: NATS_CLAIM_VERSION,\n pub: input.permissions.pub,\n sub: input.permissions.sub,\n // Unlimited subscriptions / data / payload (the workspace subject scope, NOT\n // a connection-resource quota, is the boundary here).\n subs: -1,\n data: -1,\n payload: -1,\n };\n\n const claims: Record<string, unknown> = {\n jti: \"\",\n iat: nowSeconds,\n iss: accountPublicKey,\n name: input.name,\n sub: input.userPublicKey,\n // SERVER-config-mode placement: nats-server reads the embedded user JWT's `aud`\n // as the target account NAME (the configured `auth_callout.account`). This is\n // how the authenticated user binds to that account; the workspace isolation is\n // then carried by the pub/sub permissions below.\n aud: input.audienceAccount,\n nats: natsBlock,\n };\n if (typeof input.expiresAtSeconds === \"number\") {\n claims.exp = input.expiresAtSeconds;\n }\n return encodeJwt(claims, accountKey);\n}\n\n/**\n * Input to mint the authorization RESPONSE JWT the responder publishes back on the\n * request's reply subject (ADR-26 §3).\n * - `userPublicKey` — the request's `user_nkey`; the response `sub`.\n * - `serverId` — the request's `nats.server_id.id` (the server's public key); the\n * response `aud`.\n * - `accountSeed` — the callout account signing seed; signs the response and is\n * its `iss` (public key). NEVER logged.\n * - `userJwt` — the embedded signed user JWT (omit on a denial).\n * - `error` — a human-readable denial message (omit on success). When present the\n * server denies the connection.\n */\nexport interface MintAuthResponseInput {\n userPublicKey: string;\n serverId: string;\n accountSeed: string;\n userJwt?: string;\n error?: string;\n}\n\n/**\n * Mint the signed authorization-response JWT. On success it carries the embedded\n * user JWT (`nats.jwt`); on denial it carries `nats.error` and NO user JWT, which\n * makes nats-server refuse the connection. Signed by the callout account key (its\n * public key is `iss`); `sub` is the user_nkey, `aud` is the server id.\n */\nexport function mintAuthResponse(input: MintAuthResponseInput): string {\n const accountKey = nkeys.fromSeed(Buffer.from(input.accountSeed)) as unknown as NkeyPair;\n const accountPublicKey = accountKey.getPublicKey();\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n const natsBlock: Record<string, unknown> = {\n type: AUTH_RESPONSE_CLAIM_TYPE,\n version: NATS_CLAIM_VERSION,\n };\n if (input.userJwt) {\n natsBlock.jwt = input.userJwt;\n }\n if (input.error) {\n natsBlock.error = input.error;\n }\n\n const claims: Record<string, unknown> = {\n jti: \"\",\n iat: nowSeconds,\n iss: accountPublicKey,\n // The response `aud` MUST be the SERVER public key in server-config mode\n // (nats-server validates \"Audience must be a server public key\"). The\n // authenticated user is placed into the configured `auth_callout.account` (the\n // SAME account the responder + the privileged control plane connect into), so\n // `agent.<ws>.<id>.rpc` request/reply routes; the workspace isolation is carried\n // entirely by the user JWT's pub/sub subject permissions (NOT by cross-account\n // placement, which server-config-mode nats does not support — nats-io#4335).\n aud: input.serverId,\n sub: input.userPublicKey,\n nats: natsBlock,\n };\n return encodeJwt(claims, accountKey);\n}\n\n/**\n * The fields the responder needs out of the authorization REQUEST JWT (ADR-26 §2).\n * The request is itself a NATS JWT (`header.payload.signature`) the server signs;\n * we only DECODE it (the server proves its own identity by the connection, and the\n * embedded `auth_token` is independently HMAC-verified), so we read the payload\n * without re-verifying the server signature.\n */\nexport interface DecodedAuthRequest {\n /** The public user nkey the response user JWT MUST be `sub`-scoped to. */\n userNkey: string;\n /** The server's public id — the response `aud`. */\n serverId: string;\n /** The connect `auth_token` the client presented (our `oge_` bearer), if any. */\n authToken: string | undefined;\n /** The connect username, if any (unused today; present for completeness). */\n user: string | undefined;\n}\n\n/**\n * Decode the authorization-request JWT payload (the middle base64url segment). The\n * request shape (ADR-26 §2): `nats.user_nkey`, `nats.server_id.id`, and the\n * presented connect options under `nats.connect_opts` (`auth_token` / `user`).\n * Returns null on a malformed token so the caller can deny cleanly.\n */\nexport function decodeAuthRequest(token: string): DecodedAuthRequest | null {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(Buffer.from(parts[1]!, \"base64url\").toString(\"utf8\"));\n } catch {\n return null;\n }\n if (typeof payload !== \"object\" || payload === null) {\n return null;\n }\n const nats = (payload as { nats?: unknown }).nats;\n if (typeof nats !== \"object\" || nats === null) {\n return null;\n }\n const natsObj = nats as {\n user_nkey?: unknown;\n server_id?: { id?: unknown } | unknown;\n connect_opts?: { auth_token?: unknown; user?: unknown } | unknown;\n };\n const userNkey = typeof natsObj.user_nkey === \"string\" ? natsObj.user_nkey : null;\n if (!userNkey) {\n return null;\n }\n const serverIdRaw =\n typeof natsObj.server_id === \"object\" && natsObj.server_id !== null\n ? (natsObj.server_id as { id?: unknown }).id\n : undefined;\n const serverId = typeof serverIdRaw === \"string\" ? serverIdRaw : \"\";\n const connectOpts =\n typeof natsObj.connect_opts === \"object\" && natsObj.connect_opts !== null\n ? (natsObj.connect_opts as { auth_token?: unknown; user?: unknown })\n : {};\n const authToken = typeof connectOpts.auth_token === \"string\" ? connectOpts.auth_token : undefined;\n const user = typeof connectOpts.user === \"string\" ? connectOpts.user : undefined;\n return { userNkey, serverId, authToken, user };\n}\n\n/**\n * Build the workspace-scoped permission set for an agent: it may publish + subscribe\n * ONLY `agent.<workspaceId>.>` (its own RPC/event/hello subtree) and the reply\n * `_INBOX.>` subtree (so request/reply round-trips work). Everything else is\n * implicitly denied (an allow-list with no other entries IS the deny-all-else).\n *\n * THE isolation assertion (§17): with `workspaceId=A`, the returned allow lists name\n * only `agent.A.>` — so a connection bearing this credential is rejected by\n * nats-server the instant it tries to pub/sub `agent.B.>`. This is the per-workspace\n * tenancy boundary, enforced cryptographically by the signed JWT, not by naming.\n */\nexport function workspaceAgentPermissions(workspaceId: string): NatsPermissions {\n const agentScope = `agent.${workspaceId}.>`;\n // The reply-inbox subtree must be reachable for request/reply (the control plane\n // requests on agent.<ws>.<id>.rpc with a reply inbox; the agent responds there).\n const inboxScope = \"_INBOX.>\";\n return {\n pub: { allow: [agentScope, inboxScope] },\n sub: { allow: [agentScope, inboxScope] },\n };\n}\n"],"mappings":";AACA,SAAS,qBAAqB,sBAA4D;AAC1F,SAAS,SAAS,iBAA2F;;;ACA7G,IAAM,0BAA0B,oBAAI,IAAI;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAWM,SAAS,2BAA2B,QAAwC;AACjF,QAAM,YAA4B,CAAC;AACnC,MAAI,MAAuB;AAE3B,QAAM,QAAQ,MAAM;AAClB,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AACA,cAAU,KAAK;AAAA,MACb,GAAG,IAAI;AAAA,MACP,SAAS,IAAI,MAAM,SAAS,iCAGxB;AAAA,QACE,OAAO,IAAI;AAAA,QACX,gBAAgB,IAAI;AAAA,QACpB,GAAI,IAAI,kBAAkB,SAAY,EAAE,QAAQ,IAAI,cAAc,IAAI,CAAC;AAAA,QACvE,GAAI,IAAI,qBAAqB,SAAY,EAAE,WAAW,IAAI,iBAAiB,IAAI,CAAC;AAAA,QAChF,GAAI,IAAI,gBAAgB,SAAY,EAAE,MAAM,IAAI,YAAY,IAAI,CAAC;AAAA,MACnE,IACA;AAAA,QACE,MAAM,IAAI;AAAA,QACV,gBAAgB,IAAI;AAAA,MACtB;AAAA,IACN,CAAC;AACD,UAAM;AAAA,EACR;AAEA,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,mBAAmB,KAAK,GAAG;AAC9B,YAAM;AACN,gBAAU,KAAK,KAAK;AACpB;AAAA,IACF;AAEA,UAAM,YAAY,MAAM,SAAS;AACjC,UAAM,cAAc,YAAY,iBAAiB,MAAM,OAAO,IAAI;AAClE,UAAM,gBAAgB,YAAY,mBAAmB,MAAM,SAAS,QAAQ,IAAI;AAChF,UAAM,mBAAmB,YAAY,mBAAmB,MAAM,SAAS,WAAW,IAAI;AACtF,QACE,OACG,aAAa,IAAI,OAAO,OAAO,IAAI,aAAa,WAAW,KAC3D,IAAI,kBAAkB,iBACtB,IAAI,qBAAqB,kBAC5B;AACA,UAAI,QAAQ,UAAU,KAAK;AAC3B,UAAI,eAAe,MAAM;AACzB;AAAA,IACF;AAEA,UAAM;AACN,UAAM;AAAA,MACJ,OAAO;AAAA,MACP,cAAc,MAAM;AAAA,MACpB,MAAM,UAAU,KAAK;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM;AACN,SAAO;AACT;AAEA,SAAS,mBAAmB,OAA8B;AACxD,SAAO,wBAAwB,IAAI,MAAM,IAAI;AAC/C;AAEA,SAAS,aACP,OACA,MACA,kBACA,iBACS;AACT,MAAI,MAAM,SAAS,KAAK,MAAM;AAC5B,WAAO;AAAA,EACT;AACA,OAAK,MAAM,UAAU,WAAW,KAAK,UAAU,OAAO;AACpD,WAAO;AAAA,EACT;AACA,SAAO,MAAM,SAAS,kCAAkC,qBAAqB;AAC/E;AAEA,SAAS,UAAU,OAA6B;AAC9C,MAAI,MAAM,SAAS,yBAAyB;AAC1C,WAAO,cAAc,MAAM,OAAO;AAAA,EACpC;AACA,QAAM,UAAU,SAAS,MAAM,OAAO;AACtC,MAAI,MAAM,SAAS,gCAAgC;AAGjD,eAAW,OAAO,CAAC,SAAS,QAAQ,QAAQ,GAAY;AACtD,UAAI,OAAO,QAAQ,GAAG,MAAM,UAAU;AACpC,eAAO,QAAQ,GAAG;AAAA,MACpB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AAC3D;AAEA,SAAS,cAAc,SAA0B;AAC/C,QAAM,SAAS,SAAS,OAAO;AAC/B,MAAI,OAAO,OAAO,SAAS,UAAU;AACnC,WAAO,OAAO;AAAA,EAChB;AACA,QAAM,UAAU,SAAS,SAAS,OAAO,IAAI,EAAE,OAAO,EAAE;AACxD,MAAI,CAAC,MAAM,QAAQ,OAAO,GAAG;AAC3B,WAAO;AAAA,EACT;AACA,SAAO,QACJ,IAAI,CAAC,SAAS;AACb,UAAM,OAAO,SAAS,IAAI,EAAE;AAC5B,WAAO,OAAO,SAAS,WAAW,OAAO;AAAA,EAC3C,CAAC,EACA,KAAK,EAAE;AACZ;AAEA,SAAS,iBAAiB,SAAsC;AAC9D,QAAM,OAAO,SAAS,OAAO,EAAE;AAC/B,SAAO,OAAO,SAAS,WAAW,OAAO;AAC3C;AAEA,SAAS,mBAAmB,SAAkB,KAAiD;AAC7F,QAAM,QAAQ,SAAS,OAAO,EAAE,GAAG;AACnC,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAEA,SAAS,SAAS,OAAyC;AACzD,SAAO,SAAS,OAAO,UAAU,WAAW,QAAmC,CAAC;AAClF;;;ACrHA,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AAItB,IAAM,aAAa,EAAE,KAAK,OAAO,KAAK,eAAe;AAGrD,IAAM,kBAAkB;AACxB,IAAM,2BAA2B;AACjC,IAAM,qBAAqB;AA2B3B,SAAS,gBAAgB,OAA2B;AAClD,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,WAAW;AAChD;AAIA,IAAM,kBAAkB;AACxB,SAAS,gBAAgB,OAA2B;AAClD,MAAI,OAAO;AACX,MAAI,QAAQ;AACZ,MAAI,MAAM;AACV,aAAW,QAAQ,OAAO;AACxB,YAAS,SAAS,IAAK;AACvB,YAAQ;AACR,WAAO,QAAQ,GAAG;AAChB,cAAQ;AACR,aAAO,gBAAiB,UAAU,OAAQ,EAAE;AAAA,IAC9C;AAAA,EACF;AACA,MAAI,OAAO,GAAG;AACZ,WAAO,gBAAiB,SAAU,IAAI,OAAS,EAAE;AAAA,EACnD;AACA,SAAO;AACT;AAQA,SAAS,WAAW,oBAAoC;AACtD,QAAM,OAAO,KAAK,UAAU,kBAAkB;AAC9C,QAAM,SAAS,WAAW,YAAY,EAAE,OAAO,MAAM,MAAM,EAAE,OAAO;AACpE,SAAO,gBAAgB,MAAM;AAC/B;AAQA,SAAS,UAAU,QAAiC,YAA8B;AAEhF,QAAM,eAAe,EAAE,GAAG,QAAQ,KAAK,GAAG;AAC1C,QAAM,MAAM,WAAW,YAAY;AACnC,QAAM,cAAc,EAAE,GAAG,QAAQ,IAAI;AAErC,QAAM,SAAS,gBAAgB,OAAO,KAAK,KAAK,UAAU,UAAU,GAAG,MAAM,CAAC;AAC9E,QAAM,UAAU,gBAAgB,OAAO,KAAK,KAAK,UAAU,WAAW,GAAG,MAAM,CAAC;AAChF,QAAM,eAAe,GAAG,MAAM,IAAI,OAAO;AACzC,QAAM,YAAY,WAAW,KAAK,OAAO,KAAK,cAAc,MAAM,CAAC;AACnE,SAAO,GAAG,YAAY,IAAI,gBAAgB,SAAS,CAAC;AACtD;AA+BO,SAAS,YAAY,OAAiC;AAC3D,QAAM,aAAa,MAAM,SAAS,OAAO,KAAK,MAAM,WAAW,CAAC;AAChE,QAAM,mBAAmB,WAAW,aAAa;AACjD,QAAM,aAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAE/C,QAAM,YAAqC;AAAA,IACzC,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK,MAAM,YAAY;AAAA,IACvB,KAAK,MAAM,YAAY;AAAA;AAAA;AAAA,IAGvB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AAEA,QAAM,SAAkC;AAAA,IACtC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,KAAK,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,IAKX,KAAK,MAAM;AAAA,IACX,MAAM;AAAA,EACR;AACA,MAAI,OAAO,MAAM,qBAAqB,UAAU;AAC9C,WAAO,MAAM,MAAM;AAAA,EACrB;AACA,SAAO,UAAU,QAAQ,UAAU;AACrC;AA4BO,SAAS,iBAAiB,OAAsC;AACrE,QAAM,aAAa,MAAM,SAAS,OAAO,KAAK,MAAM,WAAW,CAAC;AAChE,QAAM,mBAAmB,WAAW,aAAa;AACjD,QAAM,aAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAE/C,QAAM,YAAqC;AAAA,IACzC,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AACA,MAAI,MAAM,SAAS;AACjB,cAAU,MAAM,MAAM;AAAA,EACxB;AACA,MAAI,MAAM,OAAO;AACf,cAAU,QAAQ,MAAM;AAAA,EAC1B;AAEA,QAAM,SAAkC;AAAA,IACtC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQL,KAAK,MAAM;AAAA,IACX,KAAK,MAAM;AAAA,IACX,MAAM;AAAA,EACR;AACA,SAAO,UAAU,QAAQ,UAAU;AACrC;AA0BO,SAAS,kBAAkB,OAA0C;AAC1E,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO;AAAA,EACT;AACA,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,OAAO,KAAK,MAAM,CAAC,GAAI,WAAW,EAAE,SAAS,MAAM,CAAC;AAAA,EAC3E,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,WAAO;AAAA,EACT;AACA,QAAM,OAAQ,QAA+B;AAC7C,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,WAAO;AAAA,EACT;AACA,QAAM,UAAU;AAKhB,QAAM,WAAW,OAAO,QAAQ,cAAc,WAAW,QAAQ,YAAY;AAC7E,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AACA,QAAM,cACJ,OAAO,QAAQ,cAAc,YAAY,QAAQ,cAAc,OAC1D,QAAQ,UAA+B,KACxC;AACN,QAAM,WAAW,OAAO,gBAAgB,WAAW,cAAc;AACjE,QAAM,cACJ,OAAO,QAAQ,iBAAiB,YAAY,QAAQ,iBAAiB,OAChE,QAAQ,eACT,CAAC;AACP,QAAM,YAAY,OAAO,YAAY,eAAe,WAAW,YAAY,aAAa;AACxF,QAAM,OAAO,OAAO,YAAY,SAAS,WAAW,YAAY,OAAO;AACvE,SAAO,EAAE,UAAU,UAAU,WAAW,KAAK;AAC/C;AAaO,SAAS,0BAA0B,aAAsC;AAC9E,QAAM,aAAa,SAAS,WAAW;AAGvC,QAAM,aAAa;AACnB,SAAO;AAAA,IACL,KAAK,EAAE,OAAO,CAAC,YAAY,UAAU,EAAE;AAAA,IACvC,KAAK,EAAE,OAAO,CAAC,YAAY,UAAU,EAAE;AAAA,EACzC;AACF;;;AF/LA,SAAS,WAAAA,UAAS,SAAAC,cAAkC;AA3IpD,IAAM,QAAQ,UAA4C;AAW1D,IAAM,eAAsC;AAAA,EAC1C,OAAO,MAAM;AAAA,EAAC;AAAA,EACd,MAAM,MAAM;AAAA,EAAC;AACf;AA0BA,IAAM,oBAAoB;AAAA,EACxB,WAAW;AAAA,EACX,sBAAsB;AAAA,EACtB,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,YAAY;AACd;AAQA,SAAS,sBAAsB,SAA+C;AAC5E,SAAO,EAAE,GAAG,mBAAmB,GAAG,QAAQ;AAC5C;AAGA,IAAM,2BAA2B;AAWjC,eAAe,iBAAiB,IAAoB,WAAkC;AACpF,MAAI;AACJ,QAAM,UAAU,IAAI,QAAc,CAAC,YAAY;AAC7C,YAAQ,WAAW,SAAS,SAAS;AAAA,EACvC,CAAC;AACD,MAAI;AACF,UAAM,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,MAAM,MAAS,GAAG,OAAO,CAAC;AAAA,EACjE,UAAE;AACA,QAAI,OAAO;AACT,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AACF;AASA,SAAS,oBACP,IACA,OACA,SAAsB,cACtB,UACM;AACN,QAAM,YAAY;AAChB,QAAI;AACF,uBAAiB,UAAU,GAAG,OAAO,GAAG;AACtC,mBAAW,OAAO,IAAI;AACtB,cAAM,aAAa,EAAE,OAAO,QAAQ,OAAO,MAAM,MAAM,OAAO,KAAK;AACnE,YAAI,iBAAiB,OAAO,IAAI,GAAG;AACjC,WAAC,OAAO,QAAQ,aAAa,MAAM,0BAA0B,UAAU;AAAA,QACzE,OAAO;AACL,WAAC,OAAO,SAAS,aAAa,OAAO,0BAA0B,UAAU;AAAA,QAC3E;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAGR;AAAA,EACF,GAAG;AACL;AAEA,SAAS,iBAAiB,MAAuB;AAC/C,SAAO,SAAS,gBAAgB,SAAS,WAAW,SAAS;AAC/D;AAmGA,eAAsB,mBACpB,SACA,MACA,UAA2B,CAAC,GACT;AACnB,QAAM,iBAAoC,EAAE,SAAS,QAAQ;AAC7D,MAAI,MAAM;AACR,mBAAe,OAAO,KAAK;AAC3B,mBAAe,OAAO,KAAK;AAAA,EAC7B;AACA,QAAM,KAAK,MAAM,QAAQ,sBAAsB,cAAc,CAAC;AAC9D,MAAI,YAAY;AAChB,sBAAoB,IAAI,aAAa,QAAQ,QAAQ,CAAC,SAAS;AAC7D,QAAI,SAAS,gBAAgB,SAAS,kBAAkB,SAAS,qBAAqB,SAAS,SAAS;AACtG,kBAAY;AAAA,IACd,WAAW,SAAS,aAAa,SAAS,aAAa;AACrD,kBAAY;AAAA,IACd;AAAA,EACF,CAAC;AACD,QAAM,oBAAuC;AAAA,IAC3C,SAAS,OAAO,SAAS,SAAS,SAAS,aAAa,IAAI,SAAS,SAAS,KAAK,OAAO;AAAA,EAC5F;AACA,SAAO;AAAA,IACL,SAAS,OAAO,aAAa,WAAW,WAAW;AACjD,UAAI,OAAO,WAAW,GAAG;AACvB;AAAA,MACF;AASA,UAAI;AACF,WAAG,QAAQ,eAAe,aAAa,SAAS,GAAG,MAAM,OAAO,EAAE,aAAa,WAAW,OAAO,CAAC,CAAC;AAAA,MACrG,SAAS,OAAO;AAGd,SAAC,QAAQ,QAAQ,QAAQ,aAAa,MAAM,0FAA0F;AAAA,UACpI;AAAA,UACA;AAAA,UACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC9D,CAAC;AACD;AAAA,MACF;AACA,YAAM,iBAAiB,IAAI,wBAAwB;AAAA,IACrD;AAAA,IACA,WAAW,OAAO,aAAa,WAAW,aAAa,iBAAiB,IAAI,aAAa,WAAW,QAAQ;AAAA,IAC5G,SAAS,OAAO,SAAS,SAAS,SAAS,aAAa,IAAI,SAAS,SAAS,KAAK,SAAS;AAAA,IAC5F,mBAAmB,CAAC,SAAS,YAAY,kBAAkB,IAAI,SAAS,OAAO;AAAA,IAC/E,sBAAsB,CAAC,SAAS,YAAY,qBAAqB,IAAI,SAAS,OAAO;AAAA,IACrF,sBAAsB,MAAM;AAAA,IAC5B,aAAa,MAAM,aAAa,CAAC,GAAG,SAAS,KAAK,CAAC,GAAG,WAAW;AAAA,IACjE,OAAO,YAAY;AACjB,YAAM,GAAG,MAAM;AAAA,IACjB;AAAA,EACF;AACF;AAmCA,eAAsB,0BACpB,SACA,MACA,SACA,SACA,UAAmD,CAAC,GACtB;AAC9B,QAAM,iBAAoC,EAAE,SAAS,QAAQ;AAC7D,MAAI,QAAQ,MAAM;AAChB,mBAAe,OAAO,QAAQ;AAAA,EAChC;AACA,MAAI,KAAK,SAAS,iBAAiB;AACjC,mBAAe,OAAO,KAAK;AAC3B,mBAAe,OAAO,KAAK;AAAA,EAC7B,WAAW,KAAK,SAAS,SAAS;AAChC,mBAAe,QAAQ,KAAK;AAAA,EAC9B;AACA,QAAM,KAAK,MAAM,QAAQ,sBAAsB,cAAc,CAAC;AAC9D,sBAAoB,IAAI,QAAQ,OAAO,gBAAgB,QAAQ,IAAI,KAAK,gBAAgB,QAAQ,MAAM;AACtG,QAAM,MAAoB,GAAG,UAAU,OAAO;AAC9C,QAAM,YAAY;AAChB,qBAAiB,OAAO,KAAK;AAC3B,UAAI,CAAC,IAAI,OAAO;AACd;AAAA,MACF;AACA,UAAI;AACF,cAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI,OAAO;AACjD,YAAI,QAAQ,KAAK;AAAA,MACnB,QAAQ;AAAA,MAGR;AAAA,IACF;AAAA,EACF,GAAG;AACH,SAAO;AAAA,IACL,OAAO,YAAY;AACjB,UAAI,YAAY;AAChB,YAAM,GAAG,MAAM;AAAA,IACjB;AAAA,EACF;AACF;AAEA,eAAsB,uBAAuB,IAAc,KAAe,aAAqB,WAAmB,QAAqD;AACrK,QAAM,WAAW,MAAM,oBAAoB,IAAI,aAAa,WAAW,MAAM;AAQ7E,MAAI;AACF,UAAM,IAAI,QAAQ,aAAa,WAAW,QAAQ;AAAA,EACpD,SAAS,OAAO;AACd,YAAQ;AAAA,MACN,oCAAoC,WAAW,IAAI,SAAS,KAAK,SAAS,MAAM;AAAA,MAChF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,IAAoB,aAAqB,WAAmB,UAAwE;AAC5J,QAAM,MAAoB,GAAG,UAAU,eAAe,aAAa,SAAS,CAAC;AAC7E,QAAM,YAAY;AAChB,qBAAiB,OAAO,KAAK;AAC3B,YAAM,UAAU,MAAM,OAAO,IAAI,IAAI;AACrC,YAAM,SAAS,YAAY,UAAU,QAAQ,SAAS,CAAC,OAAO;AAC9D,YAAM,SAAS,MAAM;AAAA,IACvB;AAAA,EACF,GAAG;AACH,SAAO,MAAM;AACX,QAAI,YAAY;AAAA,EAClB;AACF;AASA,eAAe,aAAa,IAAoB,SAAiB,SAAqB,SAAwC;AAC5H,QAAM,MAAW,MAAM,GAAG,QAAQ,SAAS,SAAS,EAAE,QAAQ,CAAC;AAC/D,SAAO,EAAE,MAAM,IAAI,KAAK;AAC1B;AASA,SAAS,kBAAkB,IAAoB,SAAiB,SAAqC;AACnG,QAAM,MAAoB,GAAG,UAAU,OAAO;AAC9C,QAAM,YAAY;AAChB,qBAAiB,OAAO,KAAK;AAG3B,UAAI,CAAC,IAAI,OAAO;AACd;AAAA,MACF;AACA,UAAI;AACF,cAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI,OAAO;AACjD,YAAI,QAAQ,KAAK;AAAA,MACnB,QAAQ;AAAA,MAIR;AAAA,IACF;AAAA,EACF,GAAG;AACH,SAAO,MAAM;AACX,QAAI,YAAY;AAAA,EAClB;AACF;AASA,SAAS,qBACP,IACA,SACA,SACY;AACZ,QAAM,MAAoB,GAAG,UAAU,OAAO;AAC9C,QAAM,YAAY;AAChB,qBAAiB,OAAO,KAAK;AAC3B,UAAI;AACF,cAAM,QAAQ,IAAI,MAAM,IAAI,OAAO;AAAA,MACrC,QAAQ;AAAA,MAGR;AAAA,IACF;AAAA,EACF,GAAG;AACH,SAAO,MAAM;AACX,QAAI,YAAY;AAAA,EAClB;AACF;AAEO,SAAS,UAAU,OAA6B;AACrD,SAAO;AAAA,IACL,OAAO,MAAM,QAAQ;AAAA,IACrB,UAAU,MAAM,IAAI;AAAA,IACpB,SAAS,KAAK,UAAU,KAAK,CAAC;AAAA,IAC9B;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;","names":["connect","nkeys"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/events",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"module": "./dist/index.js",
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
@@ -21,17 +22,14 @@
|
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "tsup",
|
|
24
|
-
"typecheck": "tsc --noEmit"
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
25
27
|
},
|
|
26
28
|
"dependencies": {
|
|
27
|
-
"@opengeni/contracts": "^0.
|
|
28
|
-
"@opengeni/db": "^0.
|
|
29
|
+
"@opengeni/contracts": "^0.9.0",
|
|
30
|
+
"@opengeni/db": "^0.6.0",
|
|
29
31
|
"nats": "^2.29.3"
|
|
30
32
|
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"tsup": "^8.5.0",
|
|
33
|
-
"typescript": "^6.0.3"
|
|
34
|
-
},
|
|
35
33
|
"repository": {
|
|
36
34
|
"type": "git",
|
|
37
35
|
"url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
|
package/src/coalesce.ts
CHANGED
|
@@ -11,6 +11,8 @@ type DeltaRun = {
|
|
|
11
11
|
lastSequence: number;
|
|
12
12
|
text: string;
|
|
13
13
|
sandboxName: string | undefined;
|
|
14
|
+
sandboxStream: string | undefined;
|
|
15
|
+
sandboxCommandId: string | undefined;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
export function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent[] {
|
|
@@ -23,13 +25,20 @@ export function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent
|
|
|
23
25
|
}
|
|
24
26
|
coalesced.push({
|
|
25
27
|
...run.first,
|
|
26
|
-
payload:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
payload: run.first.type === "sandbox.command.output.delta"
|
|
29
|
+
// Sandbox output keeps its CANONICAL field (`chunk` — the terminal and
|
|
30
|
+
// projection read it) plus the stream/commandId identity of the run.
|
|
31
|
+
? {
|
|
32
|
+
chunk: run.text,
|
|
33
|
+
coalescedUntil: run.lastSequence,
|
|
34
|
+
...(run.sandboxStream !== undefined ? { stream: run.sandboxStream } : {}),
|
|
35
|
+
...(run.sandboxCommandId !== undefined ? { commandId: run.sandboxCommandId } : {}),
|
|
36
|
+
...(run.sandboxName !== undefined ? { name: run.sandboxName } : {}),
|
|
37
|
+
}
|
|
38
|
+
: {
|
|
39
|
+
text: run.text,
|
|
40
|
+
coalescedUntil: run.lastSequence,
|
|
41
|
+
},
|
|
33
42
|
});
|
|
34
43
|
run = null;
|
|
35
44
|
};
|
|
@@ -41,8 +50,16 @@ export function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent
|
|
|
41
50
|
continue;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
const
|
|
45
|
-
|
|
53
|
+
const isSandbox = event.type === "sandbox.command.output.delta";
|
|
54
|
+
const sandboxName = isSandbox ? sandboxDeltaName(event.payload) : undefined;
|
|
55
|
+
const sandboxStream = isSandbox ? sandboxDeltaString(event.payload, "stream") : undefined;
|
|
56
|
+
const sandboxCommandId = isSandbox ? sandboxDeltaString(event.payload, "commandId") : undefined;
|
|
57
|
+
if (
|
|
58
|
+
run
|
|
59
|
+
&& sameDeltaRun(run.first, event, run.sandboxName, sandboxName)
|
|
60
|
+
&& run.sandboxStream === sandboxStream
|
|
61
|
+
&& run.sandboxCommandId === sandboxCommandId
|
|
62
|
+
) {
|
|
46
63
|
run.text += deltaText(event);
|
|
47
64
|
run.lastSequence = event.sequence;
|
|
48
65
|
continue;
|
|
@@ -54,6 +71,8 @@ export function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent
|
|
|
54
71
|
lastSequence: event.sequence,
|
|
55
72
|
text: deltaText(event),
|
|
56
73
|
sandboxName,
|
|
74
|
+
sandboxStream,
|
|
75
|
+
sandboxCommandId,
|
|
57
76
|
};
|
|
58
77
|
}
|
|
59
78
|
|
|
@@ -86,7 +105,14 @@ function deltaText(event: SessionEvent): string {
|
|
|
86
105
|
}
|
|
87
106
|
const payload = asRecord(event.payload);
|
|
88
107
|
if (event.type === "sandbox.command.output.delta") {
|
|
89
|
-
|
|
108
|
+
// `chunk` is the canonical wire field (contracts SandboxCommandOutputDeltaPayload);
|
|
109
|
+
// text/output are tolerated legacy shapes.
|
|
110
|
+
for (const key of ["chunk", "text", "output"] as const) {
|
|
111
|
+
if (typeof payload[key] === "string") {
|
|
112
|
+
return payload[key] as string;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return "";
|
|
90
116
|
}
|
|
91
117
|
return typeof payload.text === "string" ? payload.text : "";
|
|
92
118
|
}
|
|
@@ -113,6 +139,11 @@ function sandboxDeltaName(payload: unknown): string | undefined {
|
|
|
113
139
|
return typeof name === "string" ? name : undefined;
|
|
114
140
|
}
|
|
115
141
|
|
|
142
|
+
function sandboxDeltaString(payload: unknown, key: "stream" | "commandId"): string | undefined {
|
|
143
|
+
const value = asRecord(payload)[key];
|
|
144
|
+
return typeof value === "string" ? value : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
116
147
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
117
148
|
return value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
118
149
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,20 @@ import { connect, JSONCodec, type ConnectionOptions, type Msg, type NatsConnecti
|
|
|
4
4
|
|
|
5
5
|
const codec = JSONCodec<SessionBusMessage | SessionEvent>();
|
|
6
6
|
|
|
7
|
+
export type EventLogger = {
|
|
8
|
+
debug?: (message: string, attributes?: Record<string, unknown>) => void;
|
|
9
|
+
warn?: (message: string, attributes?: Record<string, unknown>) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type EventBusOptions = {
|
|
13
|
+
logger?: EventLogger;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const silentLogger: Required<EventLogger> = {
|
|
17
|
+
debug: () => {},
|
|
18
|
+
warn: () => {},
|
|
19
|
+
};
|
|
20
|
+
|
|
7
21
|
export { coalesceSessionEventDeltas } from "./coalesce";
|
|
8
22
|
|
|
9
23
|
/**
|
|
@@ -82,11 +96,22 @@ async function flushWithTimeout(nc: NatsConnection, timeoutMs: number): Promise<
|
|
|
82
96
|
* connection closes. `label` distinguishes the event-bus connection from the
|
|
83
97
|
* auth-callout responder in the logs.
|
|
84
98
|
*/
|
|
85
|
-
function logConnectionStatus(
|
|
99
|
+
function logConnectionStatus(
|
|
100
|
+
nc: NatsConnection,
|
|
101
|
+
label: string,
|
|
102
|
+
logger: EventLogger = silentLogger,
|
|
103
|
+
onStatus?: (type: string) => void,
|
|
104
|
+
): void {
|
|
86
105
|
void (async () => {
|
|
87
106
|
try {
|
|
88
107
|
for await (const status of nc.status()) {
|
|
89
|
-
|
|
108
|
+
onStatus?.(status.type);
|
|
109
|
+
const attributes = { label, status: status.type, data: status.data };
|
|
110
|
+
if (isWarnNatsStatus(status.type)) {
|
|
111
|
+
(logger.warn ?? silentLogger.warn)("NATS connection status", attributes);
|
|
112
|
+
} else {
|
|
113
|
+
(logger.debug ?? silentLogger.debug)("NATS connection status", attributes);
|
|
114
|
+
}
|
|
90
115
|
}
|
|
91
116
|
} catch {
|
|
92
117
|
// The status iterator simply ends when the connection closes; never let it
|
|
@@ -95,6 +120,10 @@ function logConnectionStatus(nc: NatsConnection, label: string): void {
|
|
|
95
120
|
})();
|
|
96
121
|
}
|
|
97
122
|
|
|
123
|
+
function isWarnNatsStatus(type: string): boolean {
|
|
124
|
+
return type === "disconnect" || type === "error" || type === "staleConnection";
|
|
125
|
+
}
|
|
126
|
+
|
|
98
127
|
export {
|
|
99
128
|
decodeAuthRequest,
|
|
100
129
|
mintAuthResponse,
|
|
@@ -180,6 +209,7 @@ export type EventBus = {
|
|
|
180
209
|
* plane injects this so the transport never opens a second connection.
|
|
181
210
|
*/
|
|
182
211
|
getRequestConnection: () => RequestConnection;
|
|
212
|
+
isConnected?: () => boolean;
|
|
183
213
|
close: () => Promise<void>;
|
|
184
214
|
};
|
|
185
215
|
|
|
@@ -194,6 +224,7 @@ export type EventBus = {
|
|
|
194
224
|
export async function createNatsEventBus(
|
|
195
225
|
natsUrl: string,
|
|
196
226
|
auth?: { user: string; pass: string },
|
|
227
|
+
options: EventBusOptions = {},
|
|
197
228
|
): Promise<EventBus> {
|
|
198
229
|
const connectOptions: ConnectionOptions = { servers: natsUrl };
|
|
199
230
|
if (auth) {
|
|
@@ -201,7 +232,14 @@ export async function createNatsEventBus(
|
|
|
201
232
|
connectOptions.pass = auth.pass;
|
|
202
233
|
}
|
|
203
234
|
const nc = await connect(withReconnectDefaults(connectOptions));
|
|
204
|
-
|
|
235
|
+
let connected = true;
|
|
236
|
+
logConnectionStatus(nc, "event-bus", options.logger, (type) => {
|
|
237
|
+
if (type === "disconnect" || type === "reconnecting" || type === "staleConnection" || type === "error") {
|
|
238
|
+
connected = false;
|
|
239
|
+
} else if (type === "connect" || type === "reconnect") {
|
|
240
|
+
connected = true;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
205
243
|
const requestConnection: RequestConnection = {
|
|
206
244
|
request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeout),
|
|
207
245
|
};
|
|
@@ -223,10 +261,11 @@ export async function createNatsEventBus(
|
|
|
223
261
|
} catch (error) {
|
|
224
262
|
// `publish()` throws synchronously only when the connection is fully
|
|
225
263
|
// CLOSED (with infinite reconnect, effectively never outside shutdown).
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
264
|
+
(options.logger?.warn ?? silentLogger.warn)("NATS live publish dropped; events are durable in the DB and reconcile on stream replay", {
|
|
265
|
+
workspaceId,
|
|
266
|
+
sessionId,
|
|
267
|
+
error: error instanceof Error ? error.message : String(error),
|
|
268
|
+
});
|
|
230
269
|
return;
|
|
231
270
|
}
|
|
232
271
|
await flushWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);
|
|
@@ -236,6 +275,7 @@ export async function createNatsEventBus(
|
|
|
236
275
|
subscribeRequests: (subject, handler) => subscribeRequests(nc, subject, handler),
|
|
237
276
|
subscribeAgentEvents: (subject, handler) => subscribeAgentEvents(nc, subject, handler),
|
|
238
277
|
getRequestConnection: () => requestConnection,
|
|
278
|
+
isConnected: () => connected && !nc.isClosed() && !nc.isDraining(),
|
|
239
279
|
close: async () => {
|
|
240
280
|
await nc.drain();
|
|
241
281
|
},
|
|
@@ -280,7 +320,7 @@ export async function createResponderConnection(
|
|
|
280
320
|
auth: NatsConnectAuth,
|
|
281
321
|
subject: string,
|
|
282
322
|
handler: RequestHandler,
|
|
283
|
-
options: { name?: string } = {},
|
|
323
|
+
options: { name?: string; logger?: EventLogger } = {},
|
|
284
324
|
): Promise<ResponderConnection> {
|
|
285
325
|
const connectOptions: ConnectionOptions = { servers: natsUrl };
|
|
286
326
|
if (options.name) {
|
|
@@ -293,7 +333,7 @@ export async function createResponderConnection(
|
|
|
293
333
|
connectOptions.token = auth.token;
|
|
294
334
|
}
|
|
295
335
|
const nc = await connect(withReconnectDefaults(connectOptions));
|
|
296
|
-
logConnectionStatus(nc, options.name ? `auth-callout:${options.name}` : "auth-callout");
|
|
336
|
+
logConnectionStatus(nc, options.name ? `auth-callout:${options.name}` : "auth-callout", options.logger);
|
|
297
337
|
const sub: Subscription = nc.subscribe(subject);
|
|
298
338
|
void (async () => {
|
|
299
339
|
for await (const msg of sub) {
|