@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,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();