@qlever-llc/trellis 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/esm/_dnt.polyfills.d.ts +7 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +1 -0
- package/esm/package.json +3 -0
- package/esm/trellis/browser.d.ts +11 -0
- package/esm/trellis/browser.d.ts.map +1 -0
- package/esm/trellis/browser.js +10 -0
- package/esm/trellis/client.d.ts +31 -0
- package/esm/trellis/client.d.ts.map +1 -0
- package/esm/trellis/client.js +13 -0
- package/esm/trellis/codec.d.ts +12 -0
- package/esm/trellis/codec.d.ts.map +1 -0
- package/esm/trellis/codec.js +60 -0
- package/esm/trellis/env.d.ts +2 -0
- package/esm/trellis/env.d.ts.map +1 -0
- package/esm/trellis/env.js +1 -0
- package/esm/trellis/errors/AuthError.d.ts +30 -0
- package/esm/trellis/errors/AuthError.d.ts.map +1 -0
- package/esm/trellis/errors/AuthError.js +65 -0
- package/esm/trellis/errors/KVError.d.ts +31 -0
- package/esm/trellis/errors/KVError.d.ts.map +1 -0
- package/esm/trellis/errors/KVError.js +46 -0
- package/esm/trellis/errors/RemoteError.d.ts +47 -0
- package/esm/trellis/errors/RemoteError.d.ts.map +1 -0
- package/esm/trellis/errors/RemoteError.js +80 -0
- package/esm/trellis/errors/TrellisError.d.ts +16 -0
- package/esm/trellis/errors/TrellisError.d.ts.map +1 -0
- package/esm/trellis/errors/TrellisError.js +15 -0
- package/esm/trellis/errors/ValidationError.d.ts +51 -0
- package/esm/trellis/errors/ValidationError.d.ts.map +1 -0
- package/esm/trellis/errors/ValidationError.js +77 -0
- package/esm/trellis/errors/index.d.ts +38 -0
- package/esm/trellis/errors/index.d.ts.map +1 -0
- package/esm/trellis/errors/index.js +26 -0
- package/esm/trellis/globals.d.ts +2 -0
- package/esm/trellis/globals.d.ts.map +1 -0
- package/esm/trellis/globals.js +8 -0
- package/esm/trellis/helpers.d.ts +12 -0
- package/esm/trellis/helpers.d.ts.map +1 -0
- package/esm/trellis/helpers.js +47 -0
- package/esm/trellis/index.d.ts +11 -0
- package/esm/trellis/index.d.ts.map +1 -0
- package/esm/trellis/index.js +6 -0
- package/esm/trellis/kv.d.ts +67 -0
- package/esm/trellis/kv.d.ts.map +1 -0
- package/esm/trellis/kv.js +326 -0
- package/esm/trellis/models/trellis/TrellisError.d.ts +43 -0
- package/esm/trellis/models/trellis/TrellisError.d.ts.map +1 -0
- package/esm/trellis/models/trellis/TrellisError.js +16 -0
- package/esm/trellis/tasks.d.ts +11 -0
- package/esm/trellis/tasks.d.ts.map +1 -0
- package/esm/trellis/tasks.js +41 -0
- package/esm/trellis/tracing.d.ts +5 -0
- package/esm/trellis/tracing.d.ts.map +1 -0
- package/esm/trellis/tracing.js +7 -0
- package/esm/trellis/trellis.d.ts +117 -0
- package/esm/trellis/trellis.d.ts.map +1 -0
- package/esm/trellis/trellis.js +710 -0
- package/package.json +49 -0
- package/script/_dnt.polyfills.d.ts +7 -0
- package/script/_dnt.polyfills.d.ts.map +1 -0
- package/script/_dnt.polyfills.js +2 -0
- package/script/package.json +3 -0
- package/script/trellis/browser.d.ts +11 -0
- package/script/trellis/browser.d.ts.map +1 -0
- package/script/trellis/browser.js +21 -0
- package/script/trellis/client.d.ts +31 -0
- package/script/trellis/client.d.ts.map +1 -0
- package/script/trellis/client.js +16 -0
- package/script/trellis/codec.d.ts +12 -0
- package/script/trellis/codec.d.ts.map +1 -0
- package/script/trellis/codec.js +66 -0
- package/script/trellis/env.d.ts +2 -0
- package/script/trellis/env.d.ts.map +1 -0
- package/script/trellis/env.js +5 -0
- package/script/trellis/errors/AuthError.d.ts +30 -0
- package/script/trellis/errors/AuthError.d.ts.map +1 -0
- package/script/trellis/errors/AuthError.js +72 -0
- package/script/trellis/errors/KVError.d.ts +31 -0
- package/script/trellis/errors/KVError.d.ts.map +1 -0
- package/script/trellis/errors/KVError.js +53 -0
- package/script/trellis/errors/RemoteError.d.ts +47 -0
- package/script/trellis/errors/RemoteError.d.ts.map +1 -0
- package/script/trellis/errors/RemoteError.js +87 -0
- package/script/trellis/errors/TrellisError.d.ts +16 -0
- package/script/trellis/errors/TrellisError.d.ts.map +1 -0
- package/script/trellis/errors/TrellisError.js +19 -0
- package/script/trellis/errors/ValidationError.d.ts +51 -0
- package/script/trellis/errors/ValidationError.d.ts.map +1 -0
- package/script/trellis/errors/ValidationError.js +84 -0
- package/script/trellis/errors/index.d.ts +38 -0
- package/script/trellis/errors/index.d.ts.map +1 -0
- package/script/trellis/errors/index.js +40 -0
- package/script/trellis/globals.d.ts +2 -0
- package/script/trellis/globals.d.ts.map +1 -0
- package/script/trellis/globals.js +11 -0
- package/script/trellis/helpers.d.ts +12 -0
- package/script/trellis/helpers.d.ts.map +1 -0
- package/script/trellis/helpers.js +54 -0
- package/script/trellis/index.d.ts +11 -0
- package/script/trellis/index.d.ts.map +1 -0
- package/script/trellis/index.js +24 -0
- package/script/trellis/kv.d.ts +67 -0
- package/script/trellis/kv.d.ts.map +1 -0
- package/script/trellis/kv.js +354 -0
- package/script/trellis/models/trellis/TrellisError.d.ts +43 -0
- package/script/trellis/models/trellis/TrellisError.d.ts.map +1 -0
- package/script/trellis/models/trellis/TrellisError.js +22 -0
- package/script/trellis/tasks.d.ts +11 -0
- package/script/trellis/tasks.d.ts.map +1 -0
- package/script/trellis/tasks.js +45 -0
- package/script/trellis/tracing.d.ts +5 -0
- package/script/trellis/tracing.d.ts.map +1 -0
- package/script/trellis/tracing.js +49 -0
- package/script/trellis/trellis.d.ts +117 -0
- package/script/trellis/trellis.d.ts.map +1 -0
- package/script/trellis/trellis.js +715 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
3
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
4
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
5
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
6
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
7
|
+
};
|
|
8
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
9
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
10
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
11
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
12
|
+
};
|
|
13
|
+
var _Trellis_instances, _Trellis_log, _Trellis_tasks, _Trellis_noResponderMaxRetries, _Trellis_noResponderRetryMs, _Trellis_authBypassMethods, _Trellis_handleRPC, _Trellis_processRPCMessage, _Trellis_respondWithError, _Trellis_handleEvent, _Trellis_escapeSubjectToken, _Trellis_createProof, _TrellisServer_version, _TrellisServer_log;
|
|
14
|
+
import { jetstream, jetstreamManager, } from "@nats-io/jetstream";
|
|
15
|
+
import { headers as natsHeaders, } from "@nats-io/nats-core";
|
|
16
|
+
import { AsyncResult, err, isErr, ok, Result, } from "@qlever-llc/trellis-result";
|
|
17
|
+
import { API as trellisCoreApi } from "@qlever-llc/trellis-sdk-core";
|
|
18
|
+
import { createNatsHeaderCarrier, extractTraceContext, injectTraceContext, SpanStatusCode, startClientSpan, startServerSpan, withSpanAsync } from "@qlever-llc/trellis-telemetry";
|
|
19
|
+
import { Pointer } from "typebox/value";
|
|
20
|
+
import { ulid } from "ulid";
|
|
21
|
+
import { encodeSchema, parse, parseSchema } from "./codec.js";
|
|
22
|
+
import { AuthError, UnexpectedError, ValidationError, } from "./errors/index.js";
|
|
23
|
+
import { RemoteError } from "./errors/RemoteError.js";
|
|
24
|
+
import { logger } from "./globals.js";
|
|
25
|
+
import { TrellisErrorDataSchema } from "./models/trellis/TrellisError.js";
|
|
26
|
+
import { TrellisTasks } from "./tasks.js";
|
|
27
|
+
/**
|
|
28
|
+
* Safely extract JSON from a NATS message.
|
|
29
|
+
* The .json() method can throw if the message data is not valid JSON.
|
|
30
|
+
*/
|
|
31
|
+
function safeJson(msg) {
|
|
32
|
+
return Result.try(() => msg.json());
|
|
33
|
+
}
|
|
34
|
+
function base64urlEncode(data) {
|
|
35
|
+
const b64 = btoa(String.fromCharCode(...data));
|
|
36
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
37
|
+
}
|
|
38
|
+
function base64urlDecode(s) {
|
|
39
|
+
const normalized = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
40
|
+
const padLen = (4 - (normalized.length % 4)) % 4;
|
|
41
|
+
const padded = normalized + "=".repeat(padLen);
|
|
42
|
+
const bin = atob(padded);
|
|
43
|
+
const out = new Uint8Array(bin.length);
|
|
44
|
+
for (let i = 0; i < bin.length; i++)
|
|
45
|
+
out[i] = bin.charCodeAt(i);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function toArrayBuffer(data) {
|
|
49
|
+
const buf = data.buffer;
|
|
50
|
+
if (buf instanceof ArrayBuffer) {
|
|
51
|
+
return buf.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
52
|
+
}
|
|
53
|
+
const copy = new Uint8Array(data.byteLength);
|
|
54
|
+
copy.set(data);
|
|
55
|
+
return copy.buffer;
|
|
56
|
+
}
|
|
57
|
+
async function sha256(data) {
|
|
58
|
+
const digest = await crypto.subtle.digest("SHA-256", toArrayBuffer(data));
|
|
59
|
+
return new Uint8Array(digest);
|
|
60
|
+
}
|
|
61
|
+
function buildProofInput(sessionKey, subject, payloadHash) {
|
|
62
|
+
const enc = new TextEncoder();
|
|
63
|
+
const sessionKeyBytes = enc.encode(sessionKey);
|
|
64
|
+
const subjectBytes = enc.encode(subject);
|
|
65
|
+
const buf = new Uint8Array(4 +
|
|
66
|
+
sessionKeyBytes.length +
|
|
67
|
+
4 +
|
|
68
|
+
subjectBytes.length +
|
|
69
|
+
4 +
|
|
70
|
+
payloadHash.length);
|
|
71
|
+
const view = new DataView(buf.buffer);
|
|
72
|
+
let offset = 0;
|
|
73
|
+
view.setUint32(offset, sessionKeyBytes.length);
|
|
74
|
+
offset += 4;
|
|
75
|
+
buf.set(sessionKeyBytes, offset);
|
|
76
|
+
offset += sessionKeyBytes.length;
|
|
77
|
+
view.setUint32(offset, subjectBytes.length);
|
|
78
|
+
offset += 4;
|
|
79
|
+
buf.set(subjectBytes, offset);
|
|
80
|
+
offset += subjectBytes.length;
|
|
81
|
+
view.setUint32(offset, payloadHash.length);
|
|
82
|
+
offset += 4;
|
|
83
|
+
buf.set(payloadHash, offset);
|
|
84
|
+
return buf;
|
|
85
|
+
}
|
|
86
|
+
const NATS_SUBJECT_TOKEN_FORBIDDEN = /[\u0000\s.*>~]/gu;
|
|
87
|
+
const DEFAULT_NO_RESPONDER_MAX_RETRIES = 2;
|
|
88
|
+
const DEFAULT_NO_RESPONDER_RETRY_MS = 200;
|
|
89
|
+
export class Trellis {
|
|
90
|
+
constructor(name, // Must be unique for a service
|
|
91
|
+
nats, auth, opts) {
|
|
92
|
+
_Trellis_instances.add(this);
|
|
93
|
+
Object.defineProperty(this, "name", {
|
|
94
|
+
enumerable: true,
|
|
95
|
+
configurable: true,
|
|
96
|
+
writable: true,
|
|
97
|
+
value: void 0
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(this, "timeout", {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
configurable: true,
|
|
102
|
+
writable: true,
|
|
103
|
+
value: void 0
|
|
104
|
+
});
|
|
105
|
+
Object.defineProperty(this, "stream", {
|
|
106
|
+
enumerable: true,
|
|
107
|
+
configurable: true,
|
|
108
|
+
writable: true,
|
|
109
|
+
value: void 0
|
|
110
|
+
});
|
|
111
|
+
Object.defineProperty(this, "nats", {
|
|
112
|
+
enumerable: true,
|
|
113
|
+
configurable: true,
|
|
114
|
+
writable: true,
|
|
115
|
+
value: void 0
|
|
116
|
+
});
|
|
117
|
+
Object.defineProperty(this, "js", {
|
|
118
|
+
enumerable: true,
|
|
119
|
+
configurable: true,
|
|
120
|
+
writable: true,
|
|
121
|
+
value: void 0
|
|
122
|
+
});
|
|
123
|
+
Object.defineProperty(this, "auth", {
|
|
124
|
+
enumerable: true,
|
|
125
|
+
configurable: true,
|
|
126
|
+
writable: true,
|
|
127
|
+
value: void 0
|
|
128
|
+
});
|
|
129
|
+
Object.defineProperty(this, "api", {
|
|
130
|
+
enumerable: true,
|
|
131
|
+
configurable: true,
|
|
132
|
+
writable: true,
|
|
133
|
+
value: void 0
|
|
134
|
+
});
|
|
135
|
+
_Trellis_log.set(this, void 0);
|
|
136
|
+
_Trellis_tasks.set(this, void 0);
|
|
137
|
+
_Trellis_noResponderMaxRetries.set(this, void 0);
|
|
138
|
+
_Trellis_noResponderRetryMs.set(this, void 0);
|
|
139
|
+
_Trellis_authBypassMethods.set(this, void 0);
|
|
140
|
+
this.name = name;
|
|
141
|
+
this.nats = nats;
|
|
142
|
+
this.js = jetstream(this.nats);
|
|
143
|
+
this.auth = auth;
|
|
144
|
+
this.api = (opts?.api ?? trellisCoreApi);
|
|
145
|
+
__classPrivateFieldSet(this, _Trellis_log, (opts?.log ?? logger).child({ lib: "trellis" }), "f");
|
|
146
|
+
this.timeout = opts?.timeout ?? 3000;
|
|
147
|
+
this.stream = opts?.stream ?? "trellis";
|
|
148
|
+
__classPrivateFieldSet(this, _Trellis_noResponderMaxRetries, opts?.noResponderRetry?.maxAttempts ??
|
|
149
|
+
DEFAULT_NO_RESPONDER_MAX_RETRIES, "f");
|
|
150
|
+
__classPrivateFieldSet(this, _Trellis_noResponderRetryMs, opts?.noResponderRetry?.baseDelayMs ??
|
|
151
|
+
DEFAULT_NO_RESPONDER_RETRY_MS, "f");
|
|
152
|
+
__classPrivateFieldSet(this, _Trellis_authBypassMethods, new Set(opts?.authBypassMethods ?? []), "f");
|
|
153
|
+
__classPrivateFieldSet(this, _Trellis_tasks, new TrellisTasks({ log: __classPrivateFieldGet(this, _Trellis_log, "f") }), "f");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Returns the underlying NATS connection.
|
|
157
|
+
*/
|
|
158
|
+
get natsConnection() {
|
|
159
|
+
return this.nats;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Makes an authenticated request to a Trellis RPC method.
|
|
163
|
+
*
|
|
164
|
+
* @template M The specific RPC method being called.
|
|
165
|
+
* @param method The name of the RPC method to call.
|
|
166
|
+
* @param input The input data for the method, conforming to its schema.
|
|
167
|
+
* @param opts Optional request-specific options.
|
|
168
|
+
* @returns A promise that resolves with a `Result` containing either the method's
|
|
169
|
+
* output or an error.
|
|
170
|
+
* @returns A `Result` object:
|
|
171
|
+
* ok: A validated reponse of method M
|
|
172
|
+
* err: RemoteError | ValidationError | UnexpectedError
|
|
173
|
+
*/
|
|
174
|
+
// TypeScript hits recursion limits on this generic surface under the app's Svelte check.
|
|
175
|
+
// The implementation still builds and is exercised by runtime validation below.
|
|
176
|
+
// @ts-expect-error
|
|
177
|
+
async request(method, input, opts) {
|
|
178
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").trace({ method: String(method), input: input }, `Calling ${method.toString()}.`);
|
|
179
|
+
const ctx = this.api["rpc"][method];
|
|
180
|
+
if (!ctx) {
|
|
181
|
+
return err(new UnexpectedError({
|
|
182
|
+
cause: new Error(`Unknown RPC method '${method.toString()}'. Did you forget to include its API module?`),
|
|
183
|
+
context: { method: method.toString() },
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
const msg = encodeSchema(ctx.input, input).take();
|
|
187
|
+
if (isErr(msg)) {
|
|
188
|
+
return msg;
|
|
189
|
+
}
|
|
190
|
+
const subject = this.template(ctx.subject, input).take();
|
|
191
|
+
if (isErr(subject)) {
|
|
192
|
+
return subject;
|
|
193
|
+
}
|
|
194
|
+
// Start a client span for this RPC request
|
|
195
|
+
const span = startClientSpan(method, subject);
|
|
196
|
+
const attempt = async () => {
|
|
197
|
+
const proof = await __classPrivateFieldGet(this, _Trellis_instances, "m", _Trellis_createProof).call(this, subject, msg);
|
|
198
|
+
const headers = natsHeaders();
|
|
199
|
+
headers.set("session-key", this.auth.sessionKey);
|
|
200
|
+
headers.set("proof", proof);
|
|
201
|
+
// Inject trace context into NATS headers for propagation
|
|
202
|
+
injectTraceContext(createNatsHeaderCarrier(headers), span);
|
|
203
|
+
// Attempt request with retry for transient "no responders" errors
|
|
204
|
+
const requestWithRetry = async () => {
|
|
205
|
+
for (let retry = 0; retry <= __classPrivateFieldGet(this, _Trellis_noResponderMaxRetries, "f"); retry++) {
|
|
206
|
+
const result = await AsyncResult.try(() => this.nats.request(subject, msg, {
|
|
207
|
+
headers,
|
|
208
|
+
timeout: opts?.timeout ?? this.timeout,
|
|
209
|
+
}));
|
|
210
|
+
if (result.isOk()) {
|
|
211
|
+
return ok((await result).take());
|
|
212
|
+
}
|
|
213
|
+
const cause = result.error.cause;
|
|
214
|
+
const message = cause instanceof Error
|
|
215
|
+
? cause.message
|
|
216
|
+
: String(cause);
|
|
217
|
+
const isNoResponders = message.includes("no responders");
|
|
218
|
+
// If it's a no-responders error and we have retries left, retry
|
|
219
|
+
if (isNoResponders && retry < __classPrivateFieldGet(this, _Trellis_noResponderMaxRetries, "f")) {
|
|
220
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").debug({ method, subject, retry }, "No responders, retrying...");
|
|
221
|
+
await new Promise((r) => setTimeout(r, __classPrivateFieldGet(this, _Trellis_noResponderRetryMs, "f") * (retry + 1)));
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// Final attempt failed or non-retryable error
|
|
225
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").warn({ method, subject, error: message }, "NATS request failed");
|
|
226
|
+
const isNatsPermission = message.includes("Permissions Violation");
|
|
227
|
+
const reason = isNatsPermission
|
|
228
|
+
? `Permission denied. You need one of these capabilities: ${ctx.callerCapabilities.join(", ")}`
|
|
229
|
+
: message;
|
|
230
|
+
return err(new UnexpectedError({
|
|
231
|
+
cause,
|
|
232
|
+
context: {
|
|
233
|
+
method,
|
|
234
|
+
subject,
|
|
235
|
+
reason,
|
|
236
|
+
requiredCapabilities: ctx.callerCapabilities,
|
|
237
|
+
noResponders: isNoResponders,
|
|
238
|
+
},
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
// Should be unreachable, but TypeScript needs explicit return
|
|
242
|
+
return err(new UnexpectedError({
|
|
243
|
+
context: { method, subject, reason: "retry loop exhausted" },
|
|
244
|
+
}));
|
|
245
|
+
};
|
|
246
|
+
const msgResult = await requestWithRetry();
|
|
247
|
+
const m = msgResult.take();
|
|
248
|
+
if (isErr(m)) {
|
|
249
|
+
return m;
|
|
250
|
+
}
|
|
251
|
+
if (m.headers?.get("status") === "error") {
|
|
252
|
+
const json = safeJson(m).take();
|
|
253
|
+
if (isErr(json)) {
|
|
254
|
+
return json;
|
|
255
|
+
}
|
|
256
|
+
const error = parse(TrellisErrorDataSchema, json).take();
|
|
257
|
+
if (isErr(error)) {
|
|
258
|
+
return error;
|
|
259
|
+
}
|
|
260
|
+
return err(new RemoteError({ error }));
|
|
261
|
+
}
|
|
262
|
+
const json = safeJson(m).take();
|
|
263
|
+
if (isErr(json)) {
|
|
264
|
+
return json;
|
|
265
|
+
}
|
|
266
|
+
const outputResult = parseSchema(ctx.output, json);
|
|
267
|
+
if (outputResult.isErr()) {
|
|
268
|
+
return outputResult;
|
|
269
|
+
}
|
|
270
|
+
const output = outputResult.take();
|
|
271
|
+
return ok(output);
|
|
272
|
+
};
|
|
273
|
+
return withSpanAsync(span, async () => {
|
|
274
|
+
try {
|
|
275
|
+
const result = await attempt();
|
|
276
|
+
const value = result.take();
|
|
277
|
+
if (isErr(value)) {
|
|
278
|
+
span.setStatus({
|
|
279
|
+
code: SpanStatusCode.ERROR,
|
|
280
|
+
message: value.error.message,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
catch (cause) {
|
|
289
|
+
const unexpected = new UnexpectedError({ cause });
|
|
290
|
+
span.setStatus({
|
|
291
|
+
code: SpanStatusCode.ERROR,
|
|
292
|
+
message: unexpected.message,
|
|
293
|
+
});
|
|
294
|
+
span.recordException(unexpected);
|
|
295
|
+
return err(unexpected);
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
span.end();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/*
|
|
303
|
+
* Mount a handler to process requests made to a specific Trellis API
|
|
304
|
+
*/
|
|
305
|
+
async mount(method, fn) {
|
|
306
|
+
__classPrivateFieldGet(this, _Trellis_tasks, "f").add(method, __classPrivateFieldGet(this, _Trellis_instances, "m", _Trellis_handleRPC).call(this, method, fn));
|
|
307
|
+
}
|
|
308
|
+
async publish(event, data) {
|
|
309
|
+
const ctx = this.api["events"][event];
|
|
310
|
+
if (!ctx) {
|
|
311
|
+
return err(new UnexpectedError({
|
|
312
|
+
cause: new Error(`Unknown event '${event.toString()}'. Did you forget to include its API module?`),
|
|
313
|
+
context: { event: event.toString() },
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
const subject = this.template(ctx.subject, data).take();
|
|
317
|
+
if (isErr(subject)) {
|
|
318
|
+
logger.error({ err: subject.error }, "Failed to template event.");
|
|
319
|
+
return subject;
|
|
320
|
+
}
|
|
321
|
+
const msg = encodeSchema(ctx.event, {
|
|
322
|
+
...data,
|
|
323
|
+
header: {
|
|
324
|
+
id: ulid(),
|
|
325
|
+
time: new Date().toISOString(),
|
|
326
|
+
},
|
|
327
|
+
}).take();
|
|
328
|
+
if (isErr(msg)) {
|
|
329
|
+
logger.error({ err: msg.error }, "Failed to encode event.");
|
|
330
|
+
return msg;
|
|
331
|
+
}
|
|
332
|
+
logger.trace({ subject }, `Publishing ${event.toString()} event.`);
|
|
333
|
+
await this.js.publish(subject, msg);
|
|
334
|
+
return ok(undefined);
|
|
335
|
+
}
|
|
336
|
+
async event(event, subjectData, fn) {
|
|
337
|
+
const ctx = this.api["events"][event];
|
|
338
|
+
if (!ctx) {
|
|
339
|
+
return err(new UnexpectedError({
|
|
340
|
+
cause: new Error(`Unknown event '${event.toString()}'. Did you forget to include its API module?`),
|
|
341
|
+
context: { event: event.toString() },
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
const jsm = await jetstreamManager(this.nats);
|
|
345
|
+
const subject = this.template(ctx.subject, subjectData, true).take();
|
|
346
|
+
if (isErr(subject))
|
|
347
|
+
return subject;
|
|
348
|
+
const consumerName = `${this.name}-${event.replaceAll(".", "_")}`;
|
|
349
|
+
const addResult = await AsyncResult.try(() => jsm.consumers.add(this.stream, {
|
|
350
|
+
durable_name: consumerName,
|
|
351
|
+
ack_policy: "explicit",
|
|
352
|
+
deliver_policy: "all",
|
|
353
|
+
filter_subjects: [subject],
|
|
354
|
+
}));
|
|
355
|
+
// If add failed (consumer already exists), try to get existing consumer info
|
|
356
|
+
const consumerInfoResult = addResult.isOk()
|
|
357
|
+
? addResult
|
|
358
|
+
: await AsyncResult.try(() => jsm.consumers.info(this.stream, consumerName));
|
|
359
|
+
const info = consumerInfoResult.take();
|
|
360
|
+
if (isErr(info))
|
|
361
|
+
return info;
|
|
362
|
+
const consumer = this.js.consumers.getConsumerFromInfo(info);
|
|
363
|
+
__classPrivateFieldGet(this, _Trellis_tasks, "f").add(event, __classPrivateFieldGet(this, _Trellis_instances, "m", _Trellis_handleEvent).call(this, event, consumer, fn));
|
|
364
|
+
return ok(undefined);
|
|
365
|
+
}
|
|
366
|
+
wait() {
|
|
367
|
+
return __classPrivateFieldGet(this, _Trellis_tasks, "f").wait();
|
|
368
|
+
}
|
|
369
|
+
// FIXME: If are validating things twice in most cases...
|
|
370
|
+
template(subject, data, allowWildcards = false) {
|
|
371
|
+
// Find all template placeholders and check if values exist
|
|
372
|
+
const placeholders = subject.match(/\{([^}]+)\}/g) || [];
|
|
373
|
+
for (const placeholder of placeholders) {
|
|
374
|
+
const key = placeholder.slice(1, -1); // Remove { and }
|
|
375
|
+
const value = Pointer.Get(data, key);
|
|
376
|
+
if ((value === undefined || value === null) && !allowWildcards) {
|
|
377
|
+
return err(new ValidationError({
|
|
378
|
+
errors: [
|
|
379
|
+
{
|
|
380
|
+
path: key,
|
|
381
|
+
message: "Missing required data for subject template",
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
context: { key },
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const result = subject.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
389
|
+
const value = Pointer.Get(data, key);
|
|
390
|
+
if (allowWildcards && value === "*") {
|
|
391
|
+
return "*";
|
|
392
|
+
}
|
|
393
|
+
if (allowWildcards && (value === undefined || value === null)) {
|
|
394
|
+
return "*";
|
|
395
|
+
}
|
|
396
|
+
return __classPrivateFieldGet(this, _Trellis_instances, "m", _Trellis_escapeSubjectToken).call(this, `${value}`);
|
|
397
|
+
});
|
|
398
|
+
return ok(result);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
_Trellis_log = new WeakMap(), _Trellis_tasks = new WeakMap(), _Trellis_noResponderMaxRetries = new WeakMap(), _Trellis_noResponderRetryMs = new WeakMap(), _Trellis_authBypassMethods = new WeakMap(), _Trellis_instances = new WeakSet(), _Trellis_handleRPC = function _Trellis_handleRPC(method, fn, subjectData = {}) {
|
|
402
|
+
// Get API details
|
|
403
|
+
const ctx = this.api["rpc"][method];
|
|
404
|
+
const subject = this.template(ctx.subject, subjectData, true).take();
|
|
405
|
+
if (isErr(subject)) {
|
|
406
|
+
return AsyncResult.lift(subject);
|
|
407
|
+
}
|
|
408
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").info({ method: String(method) }, `Mounting ${method.toString()} RPC handler`);
|
|
409
|
+
const sub = this.nats.subscribe(subject);
|
|
410
|
+
return AsyncResult.try(async () => {
|
|
411
|
+
for await (const msg of sub) {
|
|
412
|
+
const resultPromise = await __classPrivateFieldGet(this, _Trellis_instances, "m", _Trellis_processRPCMessage).call(this, method, ctx, msg, fn);
|
|
413
|
+
const result = resultPromise.take();
|
|
414
|
+
if (isErr(result)) {
|
|
415
|
+
__classPrivateFieldGet(this, _Trellis_instances, "m", _Trellis_respondWithError).call(this, msg, result.error);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
msg.respond(result);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}, _Trellis_processRPCMessage = async function _Trellis_processRPCMessage(method, ctx, msg, fn) {
|
|
422
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").debug({ method: String(method), subject: msg.subject }, "Processing RPC message");
|
|
423
|
+
// Extract trace context from incoming NATS headers
|
|
424
|
+
const parentContext = extractTraceContext(createNatsHeaderCarrier({
|
|
425
|
+
get: (k) => msg.headers?.get(k) ?? undefined,
|
|
426
|
+
set: () => { }, // Server doesn't need to set headers on incoming messages
|
|
427
|
+
}));
|
|
428
|
+
// Start a server span for this RPC handler
|
|
429
|
+
const span = startServerSpan(method, msg.subject, parentContext);
|
|
430
|
+
// Execute the handler within the span's context
|
|
431
|
+
return withSpanAsync(span, async () => {
|
|
432
|
+
const execute = async () => {
|
|
433
|
+
const jsonData = safeJson(msg).take();
|
|
434
|
+
if (isErr(jsonData)) {
|
|
435
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").warn({ method, error: jsonData.error.message }, "Failed to parse JSON");
|
|
436
|
+
span.setStatus({
|
|
437
|
+
code: SpanStatusCode.ERROR,
|
|
438
|
+
message: "Failed to parse JSON",
|
|
439
|
+
});
|
|
440
|
+
return jsonData;
|
|
441
|
+
}
|
|
442
|
+
const parsedInput = parseSchema(ctx.input, jsonData).take();
|
|
443
|
+
if (isErr(parsedInput)) {
|
|
444
|
+
span.setStatus({
|
|
445
|
+
code: SpanStatusCode.ERROR,
|
|
446
|
+
message: "Input validation failed",
|
|
447
|
+
});
|
|
448
|
+
return parsedInput;
|
|
449
|
+
}
|
|
450
|
+
let user;
|
|
451
|
+
const callerSessionKey = msg.headers?.get("session-key") ?? "";
|
|
452
|
+
const authRequired = ctx.authRequired ?? true;
|
|
453
|
+
if (!authRequired || __classPrivateFieldGet(this, _Trellis_authBypassMethods, "f").has(method)) {
|
|
454
|
+
user = {
|
|
455
|
+
id: "system",
|
|
456
|
+
origin: "trellis",
|
|
457
|
+
active: true,
|
|
458
|
+
name: "System",
|
|
459
|
+
email: "system@trellis.internal",
|
|
460
|
+
capabilities: ["service"],
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
const sessionKey = msg.headers?.get("session-key");
|
|
465
|
+
const proof = msg.headers?.get("proof");
|
|
466
|
+
if (!sessionKey) {
|
|
467
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").warn({ method }, "Missing session-key header");
|
|
468
|
+
span.setStatus({
|
|
469
|
+
code: SpanStatusCode.ERROR,
|
|
470
|
+
message: "Missing session-key",
|
|
471
|
+
});
|
|
472
|
+
return err(new AuthError({ reason: "missing_session_key" }));
|
|
473
|
+
}
|
|
474
|
+
if (!proof) {
|
|
475
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").warn({ method }, "Missing proof in request");
|
|
476
|
+
span.setStatus({
|
|
477
|
+
code: SpanStatusCode.ERROR,
|
|
478
|
+
message: "Missing proof",
|
|
479
|
+
});
|
|
480
|
+
return err(new AuthError({ reason: "missing_proof" }));
|
|
481
|
+
}
|
|
482
|
+
// Verify proof signature locally using the raw request bytes we received.
|
|
483
|
+
const payloadBytes = msg.data ?? new Uint8Array();
|
|
484
|
+
const payloadHash = await sha256(payloadBytes);
|
|
485
|
+
const proofInput = buildProofInput(sessionKey, msg.subject, payloadHash);
|
|
486
|
+
const digest = await sha256(proofInput);
|
|
487
|
+
const verifyResult = await AsyncResult.try(async () => {
|
|
488
|
+
const publicKeyRaw = base64urlDecode(sessionKey);
|
|
489
|
+
const pub = await crypto.subtle.importKey("raw", toArrayBuffer(publicKeyRaw), { name: "Ed25519" }, true, ["verify"]);
|
|
490
|
+
return crypto.subtle.verify({ name: "Ed25519" }, pub, toArrayBuffer(base64urlDecode(proof)), toArrayBuffer(digest));
|
|
491
|
+
});
|
|
492
|
+
const signatureOk = verifyResult.isOk() &&
|
|
493
|
+
(await verifyResult).take() === true;
|
|
494
|
+
if (!signatureOk) {
|
|
495
|
+
span.setStatus({
|
|
496
|
+
code: SpanStatusCode.ERROR,
|
|
497
|
+
message: "Invalid signature",
|
|
498
|
+
});
|
|
499
|
+
return err(new AuthError({
|
|
500
|
+
reason: "invalid_signature",
|
|
501
|
+
context: { sessionKey },
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
const authResult = await this.request("Auth.ValidateRequest", {
|
|
505
|
+
sessionKey,
|
|
506
|
+
proof,
|
|
507
|
+
subject: msg.subject,
|
|
508
|
+
payloadHash: base64urlEncode(payloadHash),
|
|
509
|
+
capabilities: ctx.callerCapabilities,
|
|
510
|
+
});
|
|
511
|
+
const auth = authResult.take();
|
|
512
|
+
if (isErr(auth)) {
|
|
513
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").warn({
|
|
514
|
+
method,
|
|
515
|
+
error: auth.error.message,
|
|
516
|
+
errorType: auth.error.name,
|
|
517
|
+
remoteError: auth.error instanceof RemoteError
|
|
518
|
+
? auth.error.toSerializable()
|
|
519
|
+
: undefined,
|
|
520
|
+
}, "Auth.ValidateRequest failed");
|
|
521
|
+
span.setStatus({
|
|
522
|
+
code: SpanStatusCode.ERROR,
|
|
523
|
+
message: "Auth.ValidateRequest failed",
|
|
524
|
+
});
|
|
525
|
+
return auth;
|
|
526
|
+
}
|
|
527
|
+
if (!auth.allowed) {
|
|
528
|
+
span.setStatus({
|
|
529
|
+
code: SpanStatusCode.ERROR,
|
|
530
|
+
message: "Insufficient permissions",
|
|
531
|
+
});
|
|
532
|
+
return err(new AuthError({
|
|
533
|
+
reason: "insufficient_permissions",
|
|
534
|
+
context: {
|
|
535
|
+
requiredCapabilities: ctx.callerCapabilities,
|
|
536
|
+
userCapabilities: auth.user.capabilities,
|
|
537
|
+
},
|
|
538
|
+
}));
|
|
539
|
+
}
|
|
540
|
+
if (typeof msg.reply !== "string" ||
|
|
541
|
+
!msg.reply.startsWith(`${auth.inboxPrefix}.`)) {
|
|
542
|
+
span.setStatus({
|
|
543
|
+
code: SpanStatusCode.ERROR,
|
|
544
|
+
message: "Reply subject mismatch",
|
|
545
|
+
});
|
|
546
|
+
return err(new AuthError({
|
|
547
|
+
reason: "reply_subject_mismatch",
|
|
548
|
+
context: { expected: auth.inboxPrefix, actual: msg.reply },
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
user = auth.user;
|
|
552
|
+
}
|
|
553
|
+
// Add user info to span attributes
|
|
554
|
+
span.setAttribute("user.id", user.id);
|
|
555
|
+
span.setAttribute("user.origin", user.origin);
|
|
556
|
+
const handlerResultWrapped = await AsyncResult.try(() => fn(parsedInput, {
|
|
557
|
+
user,
|
|
558
|
+
sessionKey: callerSessionKey,
|
|
559
|
+
}));
|
|
560
|
+
if (handlerResultWrapped.isErr()) {
|
|
561
|
+
const error = handlerResultWrapped.error.withContext({ method });
|
|
562
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").error({
|
|
563
|
+
method,
|
|
564
|
+
error: error.message,
|
|
565
|
+
cause: error.cause instanceof Error
|
|
566
|
+
? { message: error.cause.message, stack: error.cause.stack }
|
|
567
|
+
: error.cause,
|
|
568
|
+
}, "Handler threw unexpectedly.");
|
|
569
|
+
span.setStatus({
|
|
570
|
+
code: SpanStatusCode.ERROR,
|
|
571
|
+
message: error.message,
|
|
572
|
+
});
|
|
573
|
+
span.recordException(error);
|
|
574
|
+
return err(error);
|
|
575
|
+
}
|
|
576
|
+
const handlerResult = (await handlerResultWrapped).take();
|
|
577
|
+
const handlerOutcome = handlerResult.take();
|
|
578
|
+
if (isErr(handlerOutcome)) {
|
|
579
|
+
const handlerError = handlerOutcome.error;
|
|
580
|
+
const error = handlerError instanceof UnexpectedError ||
|
|
581
|
+
handlerError instanceof AuthError ||
|
|
582
|
+
handlerError instanceof ValidationError
|
|
583
|
+
? handlerError
|
|
584
|
+
: new UnexpectedError({ cause: handlerError });
|
|
585
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").error({
|
|
586
|
+
method,
|
|
587
|
+
error: error.message,
|
|
588
|
+
errorType: error.name,
|
|
589
|
+
cause: error.cause instanceof Error
|
|
590
|
+
? { message: error.cause.message, stack: error.cause.stack }
|
|
591
|
+
: error.cause,
|
|
592
|
+
}, "Handler returned error.");
|
|
593
|
+
span.setStatus({
|
|
594
|
+
code: SpanStatusCode.ERROR,
|
|
595
|
+
message: error.message,
|
|
596
|
+
});
|
|
597
|
+
return err(error);
|
|
598
|
+
}
|
|
599
|
+
const encoded = encodeSchema(ctx.output, handlerOutcome).take();
|
|
600
|
+
if (isErr(encoded)) {
|
|
601
|
+
span.setStatus({
|
|
602
|
+
code: SpanStatusCode.ERROR,
|
|
603
|
+
message: "Output encoding failed",
|
|
604
|
+
});
|
|
605
|
+
return encoded;
|
|
606
|
+
}
|
|
607
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
608
|
+
return ok(encoded);
|
|
609
|
+
};
|
|
610
|
+
const result = await execute();
|
|
611
|
+
span.end();
|
|
612
|
+
return result;
|
|
613
|
+
});
|
|
614
|
+
}, _Trellis_respondWithError = function _Trellis_respondWithError(msg, error) {
|
|
615
|
+
const trellisError = error instanceof UnexpectedError ||
|
|
616
|
+
error instanceof AuthError ||
|
|
617
|
+
error instanceof ValidationError ||
|
|
618
|
+
error instanceof RemoteError
|
|
619
|
+
? error
|
|
620
|
+
: new UnexpectedError({ cause: error });
|
|
621
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").error({ error: trellisError.toSerializable() }, "RPC error");
|
|
622
|
+
const errorData = trellisError.toSerializable();
|
|
623
|
+
const hdrs = natsHeaders();
|
|
624
|
+
hdrs.set("status", "error");
|
|
625
|
+
const serialized = Result.try(() => JSON.stringify(errorData));
|
|
626
|
+
if (serialized.isErr()) {
|
|
627
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").error({ error: serialized.error }, "Failed to serialize error response");
|
|
628
|
+
msg.respond('{"type":"UnexpectedError","message":"Failed to serialize error"}', { headers: hdrs });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
msg.respond(serialized.take(), { headers: hdrs });
|
|
632
|
+
}, _Trellis_handleEvent = function _Trellis_handleEvent(event, consumer, fn) {
|
|
633
|
+
const ctx = this.api["events"][event];
|
|
634
|
+
return AsyncResult.try(async () => {
|
|
635
|
+
const msgs = await consumer.consume();
|
|
636
|
+
for await (const msg of msgs) {
|
|
637
|
+
const jsonData = Result.try(() => msg.json());
|
|
638
|
+
if (jsonData.isErr()) {
|
|
639
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").error({ error: jsonData.error }, "Event parse failed");
|
|
640
|
+
msg.term();
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const m = parseSchema(ctx.event, jsonData.take()).take();
|
|
644
|
+
if (isErr(m)) {
|
|
645
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").error({ error: m.error }, "Event validation failed");
|
|
646
|
+
msg.term();
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const handlerResult = await AsyncResult.lift(fn(m));
|
|
650
|
+
if (handlerResult.isErr()) {
|
|
651
|
+
__classPrivateFieldGet(this, _Trellis_log, "f").error({ error: handlerResult.error.toSerializable(), event, subject: msg.subject }, "Event handler failed");
|
|
652
|
+
msg.nak();
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
msg.ack();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}, _Trellis_escapeSubjectToken = function _Trellis_escapeSubjectToken(token) {
|
|
659
|
+
const out = token.replace(NATS_SUBJECT_TOKEN_FORBIDDEN, (ch) => `~${ch.codePointAt(0).toString(16).toUpperCase()}~`);
|
|
660
|
+
// Protect stapRet with $ due to NATS internal use of it
|
|
661
|
+
if (out.length === 0 || out.startsWith("$")) {
|
|
662
|
+
return `_${out}`;
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}, _Trellis_createProof = async function _Trellis_createProof(subject, payload) {
|
|
666
|
+
const payloadBytes = new TextEncoder().encode(payload);
|
|
667
|
+
const payloadHash = await sha256(payloadBytes);
|
|
668
|
+
const input = buildProofInput(this.auth.sessionKey, subject, payloadHash);
|
|
669
|
+
const digest = await sha256(input);
|
|
670
|
+
const sigBytes = await this.auth.sign(digest);
|
|
671
|
+
return base64urlEncode(sigBytes);
|
|
672
|
+
};
|
|
673
|
+
export class TrellisServer extends Trellis {
|
|
674
|
+
constructor(name, nats, auth, opts) {
|
|
675
|
+
super(name, nats, auth, opts);
|
|
676
|
+
_TrellisServer_version.set(this, void 0);
|
|
677
|
+
_TrellisServer_log.set(this, void 0);
|
|
678
|
+
__classPrivateFieldSet(this, _TrellisServer_version, opts?.version, "f");
|
|
679
|
+
__classPrivateFieldSet(this, _TrellisServer_log, (opts?.log ?? logger).child({ lib: "trellis-server" }), "f");
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Creates an authenticated TrellisServer instance.
|
|
683
|
+
*
|
|
684
|
+
* Services connect to NATS using the session-key auth flow (see ADR):
|
|
685
|
+
* - NATS `auth_token` (aka `token`) is a JSON string `{ v: 1, sessionKey, iat, sig }`
|
|
686
|
+
* - `sig` signs SHA-256(`nats-connect:${iat}`) with the session key
|
|
687
|
+
* - `inboxPrefix` MUST be `_INBOX.${sessionKey.slice(0, 16)}`
|
|
688
|
+
*
|
|
689
|
+
* @param name Unique name for this service
|
|
690
|
+
* @param nats Existing NATS connection (already authenticated)
|
|
691
|
+
* @param auth Service session-key credentials
|
|
692
|
+
* @param opts Optional server options
|
|
693
|
+
* @returns An authenticated TrellisServer instance
|
|
694
|
+
*/
|
|
695
|
+
static create(name, nats, auth, opts) {
|
|
696
|
+
return new TrellisServer(name, nats, auth, opts);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Stops the server by clearing refresh timers and draining the NATS connection.
|
|
700
|
+
* Draining allows in-flight messages to complete before closing the connection.
|
|
701
|
+
* This method is idempotent and can be called multiple times safely.
|
|
702
|
+
*/
|
|
703
|
+
async stop() {
|
|
704
|
+
// Only drain if the connection is not already closed
|
|
705
|
+
if (!this.natsConnection.isClosed()) {
|
|
706
|
+
await this.natsConnection.drain();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
_TrellisServer_version = new WeakMap(), _TrellisServer_log = new WeakMap();
|