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