@terreno/api 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.ai/guidelines/core.md +71 -0
  2. package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
  3. package/README.md +54 -1
  4. package/dist/__tests__/versionCheckPlugin.test.js +29 -7
  5. package/dist/actions.openApi.test.js +13 -11
  6. package/dist/api.js +98 -11
  7. package/dist/api.query.test.js +31 -1
  8. package/dist/api.test.js +211 -0
  9. package/dist/auth.test.js +10 -10
  10. package/dist/betterAuth.d.ts +1 -1
  11. package/dist/consentApp.test.js +1 -0
  12. package/dist/example.js +4 -4
  13. package/dist/expressServer.d.ts +0 -22
  14. package/dist/expressServer.js +1 -125
  15. package/dist/expressServer.test.js +90 -91
  16. package/dist/githubAuth.test.js +22 -22
  17. package/dist/logger.d.ts +154 -0
  18. package/dist/logger.js +445 -26
  19. package/dist/logger.test.js +435 -0
  20. package/dist/middleware.d.ts +7 -0
  21. package/dist/middleware.js +58 -1
  22. package/dist/middleware.test.js +159 -0
  23. package/dist/openApi.test.js +10 -17
  24. package/dist/openApiBuilder.test.js +18 -10
  25. package/dist/realtime/changeStreamWatcher.d.ts +4 -4
  26. package/dist/realtime/changeStreamWatcher.js +2 -4
  27. package/dist/realtime/queryMatcher.d.ts +1 -1
  28. package/dist/realtime/queryMatcher.js +39 -14
  29. package/dist/realtime/types.d.ts +3 -3
  30. package/dist/requestContext.d.ts +61 -0
  31. package/dist/requestContext.js +74 -0
  32. package/dist/secretProviders.test.js +335 -0
  33. package/dist/terrenoApp.d.ts +27 -15
  34. package/dist/terrenoApp.js +24 -14
  35. package/dist/terrenoApp.test.js +52 -0
  36. package/dist/tests/bunSetup.js +61 -7
  37. package/dist/tests.js +27 -4
  38. package/package.json +1 -1
  39. package/src/__tests__/versionCheckPlugin.test.ts +43 -15
  40. package/src/actions.openApi.test.ts +12 -10
  41. package/src/api.query.test.ts +24 -1
  42. package/src/api.test.ts +169 -0
  43. package/src/api.ts +71 -0
  44. package/src/auth.test.ts +10 -10
  45. package/src/betterAuth.ts +1 -1
  46. package/src/consentApp.test.ts +1 -0
  47. package/src/example.ts +4 -4
  48. package/src/expressServer.test.ts +82 -85
  49. package/src/expressServer.ts +1 -213
  50. package/src/githubAuth.test.ts +22 -22
  51. package/src/logger.test.ts +466 -1
  52. package/src/logger.ts +477 -14
  53. package/src/middleware.test.ts +74 -2
  54. package/src/middleware.ts +57 -0
  55. package/src/openApi.test.ts +10 -17
  56. package/src/openApiBuilder.test.ts +18 -10
  57. package/src/realtime/changeStreamWatcher.ts +15 -10
  58. package/src/realtime/queryMatcher.ts +54 -27
  59. package/src/realtime/types.ts +4 -4
  60. package/src/requestContext.ts +86 -0
  61. package/src/secretProviders.test.ts +219 -1
  62. package/src/terrenoApp.test.ts +38 -0
  63. package/src/terrenoApp.ts +37 -15
  64. package/src/tests/bunSetup.ts +16 -3
  65. package/src/tests.ts +17 -4
package/dist/logger.js CHANGED
@@ -43,6 +43,17 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  return result;
44
44
  };
45
45
  })();
46
+ var __values = (this && this.__values) || function(o) {
47
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
48
+ if (m) return m.call(o);
49
+ if (o && typeof o.length === "number") return {
50
+ next: function () {
51
+ if (o && i >= o.length) o = void 0;
52
+ return { value: o && o[i++], done: !o };
53
+ }
54
+ };
55
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
56
+ };
46
57
  var __read = (this && this.__read) || function (o, n) {
47
58
  var m = typeof Symbol === "function" && o[Symbol.iterator];
48
59
  if (!m) return o;
@@ -68,23 +79,39 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
68
79
  }
69
80
  return to.concat(ar || Array.prototype.slice.call(from));
70
81
  };
71
- var __values = (this && this.__values) || function(o) {
72
- var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
73
- if (m) return m.call(o);
74
- if (o && typeof o.length === "number") return {
75
- next: function () {
76
- if (o && i >= o.length) o = void 0;
77
- return { value: o && o[i++], done: !o };
78
- }
79
- };
80
- throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
81
- };
82
82
  var __importDefault = (this && this.__importDefault) || function (mod) {
83
83
  return (mod && mod.__esModule) ? mod : { "default": mod };
84
84
  };
85
85
  Object.defineProperty(exports, "__esModule", { value: true });
86
- exports.setupLogging = exports.logger = exports.winstonLogger = void 0;
86
+ exports.setupLogging = exports.createFeatureFlaggedLogger = exports.createScopedLogger = exports.logger = exports.winstonLogger = exports.formatLogContextSuffix = void 0;
87
+ /**
88
+ * Backend logging for `@terreno/api`.
89
+ *
90
+ * Three building blocks cooperate so a single request or background job can be followed across
91
+ * many log lines, both in plain-text consoles and in structured transports (Google Cloud Logging,
92
+ * Sentry):
93
+ *
94
+ * - **{@link logger}** – the global logger (`debug` / `info` / `warn` / `error` / `catch`). Use it
95
+ * for one-off messages.
96
+ * - **{@link createScopedLogger}** – returns a logger that prepends a stable `prefix` and/or
97
+ * attaches `labels` (workflow dimensions such as `invoiceId`) to every line. Use it when a
98
+ * handler, job, or service runs multiple steps that should share identifiers.
99
+ * - **{@link createFeatureFlaggedLogger}** – wraps any {@link ScopedLogger} behind an
100
+ * `isEnabled()` predicate so verbose diagnostics can be toggled with a feature flag or env var
101
+ * without a redeploy.
102
+ *
103
+ * **Correlation** is automatic: while a request/job AsyncLocalStorage scope is active (see
104
+ * `requestContext.ts` – HTTP middleware or `runWithRequestContext`), every log line is enriched
105
+ * with `requestId`, `userId`, `traceId`, etc. and a nested `terrenoRequestLog` object, regardless
106
+ * of which logger above emitted it.
107
+ *
108
+ * @see {@link createScopedLogger}
109
+ * @see {@link createFeatureFlaggedLogger}
110
+ * @see {@link formatLogContextSuffix}
111
+ * @module logger
112
+ */
87
113
  var node_fs_1 = __importDefault(require("node:fs"));
114
+ var node_path_1 = require("node:path");
88
115
  var node_util_1 = require("node:util");
89
116
  var Sentry = __importStar(require("@sentry/bun"));
90
117
  var winston_1 = __importDefault(require("winston"));
@@ -97,23 +124,95 @@ var formatWithInspect = function (val) {
97
124
  var shouldFormat = typeof val !== "string";
98
125
  return prefix + (shouldFormat ? (0, node_util_1.inspect)(val, { colors: true, depth: null }) : val);
99
126
  };
127
+ var buildTerrenoRequestLog = function (active) {
128
+ var _a;
129
+ return {
130
+ requestId: active.requestId,
131
+ userId: (_a = active.userId) !== null && _a !== void 0 ? _a : null,
132
+ };
133
+ };
134
+ var mergeActiveRequestIntoInfo = function (info, active) {
135
+ var next = __assign(__assign({}, info), { requestId: active.requestId, terrenoRequestLog: buildTerrenoRequestLog(active) });
136
+ if (active.jobId) {
137
+ next.jobId = active.jobId;
138
+ }
139
+ if (active.sessionId) {
140
+ next.sessionId = active.sessionId;
141
+ }
142
+ if (active.userId) {
143
+ next.userId = active.userId;
144
+ }
145
+ if (active.spanId) {
146
+ next.spanId = active.spanId;
147
+ }
148
+ if (active.traceId) {
149
+ next.traceId = active.traceId;
150
+ }
151
+ if (active.traceSampled !== undefined) {
152
+ next.traceSampled = active.traceSampled;
153
+ }
154
+ return next;
155
+ };
100
156
  var addRequestContextFormat = winston_1.default.format(function (info) {
101
- var context = (0, requestContext_1.getCurrentLogContext)();
102
- return __assign(__assign({}, context), info);
157
+ var active = (0, requestContext_1.getCurrentRequestContext)();
158
+ if (!active) {
159
+ return __assign({}, info);
160
+ }
161
+ return mergeActiveRequestIntoInfo(info, active);
103
162
  });
104
- var formatContext = function (info) {
163
+ /**
164
+ * Builds the ` key=value ...` suffix appended to console/file log lines after the message.
165
+ * Request-scoped fields come from AsyncLocalStorage via Winston metadata; `terrenoLabels` and
166
+ * `terrenoLogPrefix` come from {@link createScopedLogger}. Nested `terrenoRequestLog`
167
+ * (`requestId` + `userId` including `null` when anonymous) is attached on the Winston info
168
+ * object for structured transports only, not repeated in this suffix.
169
+ */
170
+ var formatLogContextSuffix = function (fields) {
171
+ var e_1, _a;
105
172
  var contextParts = [
106
- info.requestId ? "requestId=".concat(info.requestId) : undefined,
107
- info.jobId ? "jobId=".concat(info.jobId) : undefined,
108
- info.sessionId ? "sessionId=".concat(info.sessionId) : undefined,
109
- info.userId ? "userId=".concat(info.userId) : undefined,
110
- info.traceId ? "traceId=".concat(info.traceId) : undefined,
173
+ fields.requestId ? "requestId=".concat(fields.requestId) : undefined,
174
+ fields.jobId ? "jobId=".concat(fields.jobId) : undefined,
175
+ fields.sessionId ? "sessionId=".concat(fields.sessionId) : undefined,
176
+ fields.userId ? "userId=".concat(fields.userId) : undefined,
177
+ fields.traceId ? "traceId=".concat(fields.traceId) : undefined,
178
+ fields.terrenoLogPrefix ? "logPrefix=".concat(fields.terrenoLogPrefix) : undefined,
111
179
  ].filter(Boolean);
180
+ if (fields.terrenoLabels && typeof fields.terrenoLabels === "object") {
181
+ var sortedKeys = Object.keys(fields.terrenoLabels).sort();
182
+ try {
183
+ for (var sortedKeys_1 = __values(sortedKeys), sortedKeys_1_1 = sortedKeys_1.next(); !sortedKeys_1_1.done; sortedKeys_1_1 = sortedKeys_1.next()) {
184
+ var key = sortedKeys_1_1.value;
185
+ var value = fields.terrenoLabels[key];
186
+ if (value !== undefined && value !== "") {
187
+ contextParts.push("".concat(key, "=").concat(value));
188
+ }
189
+ }
190
+ }
191
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
192
+ finally {
193
+ try {
194
+ if (sortedKeys_1_1 && !sortedKeys_1_1.done && (_a = sortedKeys_1.return)) _a.call(sortedKeys_1);
195
+ }
196
+ finally { if (e_1) throw e_1.error; }
197
+ }
198
+ }
112
199
  if (contextParts.length === 0) {
113
200
  return "";
114
201
  }
115
202
  return " ".concat(contextParts.join(" "));
116
203
  };
204
+ exports.formatLogContextSuffix = formatLogContextSuffix;
205
+ var formatContext = function (info) {
206
+ return (0, exports.formatLogContextSuffix)({
207
+ jobId: info.jobId,
208
+ requestId: info.requestId,
209
+ sessionId: info.sessionId,
210
+ terrenoLabels: info.terrenoLabels,
211
+ terrenoLogPrefix: info.terrenoLogPrefix,
212
+ traceId: info.traceId,
213
+ userId: info.userId,
214
+ });
215
+ };
117
216
  // Winston doesn't operate like console.log by default, e.g. `logger.error('error',
118
217
  // error)` only prints the message and no args. Add handling for all the args,
119
218
  // while also supporting splat logging.
@@ -131,6 +230,43 @@ var printf = function (timestamp) {
131
230
  return "".concat(info.level, ": ").concat(msg).concat(context, " ").concat(rest);
132
231
  };
133
232
  };
233
+ var terrenoDevJsonlAttached = false;
234
+ var shouldAttachTerrenoDevJsonl = function () {
235
+ if (process.env.TERRENO_LOG_FILE === "false" || process.env.TERRENO_LOG_FILE === "0") {
236
+ return false;
237
+ }
238
+ if (process.env.TERRENO_LOG_FILE === "true" || process.env.TERRENO_LOG_FILE === "1") {
239
+ return true;
240
+ }
241
+ return process.env.NODE_ENV !== "production";
242
+ };
243
+ var attachTerrenoDevJsonlTransportIfEnabled = function (logger, options) {
244
+ if (options === null || options === void 0 ? void 0 : options.disable) {
245
+ return;
246
+ }
247
+ if (!shouldAttachTerrenoDevJsonl()) {
248
+ return;
249
+ }
250
+ if (terrenoDevJsonlAttached) {
251
+ return;
252
+ }
253
+ var logDir = (0, node_path_1.join)(process.cwd(), ".terreno", "logs");
254
+ if (!node_fs_1.default.existsSync(logDir)) {
255
+ node_fs_1.default.mkdirSync(logDir, { recursive: true });
256
+ }
257
+ var maxBytes = 5 * 1024 * 1024;
258
+ logger.add(new winston_1.default.transports.File({
259
+ filename: (0, node_path_1.join)(logDir, "app.log"),
260
+ format: winston_1.default.format.combine(addRequestContextFormat(), winston_1.default.format.timestamp(), winston_1.default.format.json()),
261
+ handleExceptions: true,
262
+ handleRejections: true,
263
+ level: "debug",
264
+ maxFiles: 3,
265
+ maxsize: maxBytes,
266
+ options: { flags: "a", mode: 384 },
267
+ }));
268
+ terrenoDevJsonlAttached = true;
269
+ };
134
270
  // Setup a global, default rejection handler.
135
271
  winston_1.default.add(new winston_1.default.transports.Console({
136
272
  debugStdout: true,
@@ -153,13 +289,42 @@ exports.winstonLogger = winston_1.default.createLogger({
153
289
  }),
154
290
  ],
155
291
  });
292
+ var mergeSentryLogAttributes = function (extra) {
293
+ var active = (0, requestContext_1.getCurrentRequestContext)();
294
+ var out = __assign(__assign({}, (0, requestContext_1.getCurrentLogContext)()), (extra !== null && extra !== void 0 ? extra : {}));
295
+ if (active) {
296
+ out.terrenoRequestLog = buildTerrenoRequestLog(active);
297
+ }
298
+ return out;
299
+ };
300
+ attachTerrenoDevJsonlTransportIfEnabled(exports.winstonLogger);
156
301
  // Helper function to send logs to Sentry if enabled
157
- var sendToSentry = function (message, level) {
302
+ var sendToSentry = function (message, level, extraAttributes) {
158
303
  if (process.env.USE_SENTRY_LOGGING === "true" && Sentry.logger) {
159
304
  var logWithContext = Sentry.logger[level];
160
- logWithContext(message, (0, requestContext_1.getCurrentLogContext)());
305
+ logWithContext(message, mergeSentryLogAttributes(extraAttributes));
161
306
  }
162
307
  };
308
+ /**
309
+ * Global application logger. Each method writes through Winston (console/file transports) and, when
310
+ * `USE_SENTRY_LOGGING=true`, mirrors the line to Sentry with the active request context attached.
311
+ *
312
+ * Prefer {@link createScopedLogger} when a workflow spans multiple log lines that should share a
313
+ * prefix or labels.
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * import {logger} from "@terreno/api";
318
+ *
319
+ * logger.info("Server started", {port: 4000});
320
+ * logger.warn("Slow query", {ms: 500});
321
+ * logger.error("Failed to process", {error});
322
+ * logger.debug("Request details", {body: req.body});
323
+ *
324
+ * // Convenient `.catch` handler for promises – logs and captures the exception.
325
+ * await chargeCard(id).catch(logger.catch);
326
+ * ```
327
+ */
163
328
  exports.logger = {
164
329
  // simple way to log a caught exception. e.g. promise().catch(logger.catch)
165
330
  catch: function (e) {
@@ -170,7 +335,7 @@ exports.logger = {
170
335
  Sentry.captureException(e);
171
336
  }
172
337
  else if (Sentry.logger) {
173
- Sentry.logger.error(errorMsg);
338
+ Sentry.logger.error(errorMsg, mergeSentryLogAttributes());
174
339
  }
175
340
  }
176
341
  },
@@ -207,10 +372,261 @@ exports.logger = {
207
372
  sendToSentry(msg, "warn");
208
373
  },
209
374
  };
375
+ var normalizeLogLabels = function (labels) {
376
+ var e_2, _a;
377
+ if (!labels) {
378
+ return undefined;
379
+ }
380
+ var out = {};
381
+ try {
382
+ for (var _b = __values(Object.entries(labels)), _c = _b.next(); !_c.done; _c = _b.next()) {
383
+ var _d = __read(_c.value, 2), key = _d[0], value = _d[1];
384
+ if (value === undefined || value === null) {
385
+ continue;
386
+ }
387
+ out[key] = String(value);
388
+ }
389
+ }
390
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
391
+ finally {
392
+ try {
393
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
394
+ }
395
+ finally { if (e_2) throw e_2.error; }
396
+ }
397
+ return Object.keys(out).length > 0 ? out : undefined;
398
+ };
399
+ var applyMessagePrefix = function (prefix, msg) {
400
+ var trimmed = prefix === null || prefix === void 0 ? void 0 : prefix.trim();
401
+ if (!trimmed) {
402
+ return msg;
403
+ }
404
+ return "".concat(trimmed, " ").concat(msg);
405
+ };
406
+ var buildScopedLoggerSentryExtras = function (labels, logPrefix) {
407
+ var e_3, _a;
408
+ var out = {};
409
+ if (logPrefix) {
410
+ out.terrenoLogPrefix = logPrefix;
411
+ }
412
+ if (labels) {
413
+ try {
414
+ for (var _b = __values(Object.entries(labels)), _c = _b.next(); !_c.done; _c = _b.next()) {
415
+ var _d = __read(_c.value, 2), key = _d[0], value = _d[1];
416
+ out[key] = value;
417
+ }
418
+ }
419
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
420
+ finally {
421
+ try {
422
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
423
+ }
424
+ finally { if (e_3) throw e_3.error; }
425
+ }
426
+ }
427
+ return Object.keys(out).length > 0 ? out : undefined;
428
+ };
429
+ /**
430
+ * Creates a {@link ScopedLogger} that prefixes every message and/or attaches stable `labels` to
431
+ * every line, so multi-step workflows are easy to group and search.
432
+ *
433
+ * - `prefix` is prepended to the human-readable message (easy grep / Log Explorer text search) and
434
+ * also stored as the Winston metadata field `terrenoLogPrefix`.
435
+ * - `labels` are normalized to strings and stored as the Winston metadata field `terrenoLabels`.
436
+ * They appear in the plain-text ` key=value` suffix (see {@link formatLogContextSuffix}) and as
437
+ * discrete fields on structured transports such as `@google-cloud/logging-winston`.
438
+ *
439
+ * Both ride on a Winston **child logger**, so they merge with — and never overwrite — the
440
+ * request/job correlation fields that AsyncLocalStorage injects (`requestId`, `userId`,
441
+ * `terrenoRequestLog`, etc.). Avoid label keys that collide with those framework fields:
442
+ * `requestId`, `jobId`, `sessionId`, `userId`, `traceId`, `spanId`, `terrenoLogPrefix`,
443
+ * `terrenoRequestLog`, `terrenoLabels`.
444
+ *
445
+ * If both `prefix` and `labels` are empty, the global {@link logger} is returned unchanged.
446
+ *
447
+ * @param options - Optional `prefix` token and/or `labels` dimensions for this scope.
448
+ * @returns A scoped logger sharing the same methods as the global {@link logger}.
449
+ * @see {@link createFeatureFlaggedLogger} to gate a scoped logger behind a feature flag.
450
+ *
451
+ * @example Reuse one instance for a whole workflow so every line shares identifiers
452
+ * ```typescript
453
+ * import {createScopedLogger} from "@terreno/api";
454
+ *
455
+ * const log = createScopedLogger({
456
+ * prefix: "[InvoicePay]",
457
+ * labels: {invoiceId: invoice._id.toString(), attempt: String(attemptNumber)},
458
+ * });
459
+ *
460
+ * log.info("Starting capture"); // -> "[InvoicePay] Starting capture invoiceId=... attempt=1 requestId=..."
461
+ * log.warn("Stripe rate limited, backing off");
462
+ * await capture(invoice).catch(log.catch);
463
+ * ```
464
+ */
465
+ var createScopedLogger = function (options) {
466
+ var _a;
467
+ if (options === void 0) { options = {}; }
468
+ var trimmedPrefix = ((_a = options.prefix) === null || _a === void 0 ? void 0 : _a.trim()) ? options.prefix.trim() : undefined;
469
+ var terrenoLabels = normalizeLogLabels(options.labels);
470
+ if (!trimmedPrefix && !terrenoLabels) {
471
+ return exports.logger;
472
+ }
473
+ var childDefaults = {};
474
+ if (terrenoLabels) {
475
+ childDefaults.terrenoLabels = terrenoLabels;
476
+ }
477
+ if (trimmedPrefix) {
478
+ childDefaults.terrenoLogPrefix = trimmedPrefix;
479
+ }
480
+ var base = exports.winstonLogger.child(childDefaults);
481
+ var sentryExtras = function () {
482
+ return buildScopedLoggerSentryExtras(terrenoLabels, trimmedPrefix);
483
+ };
484
+ return {
485
+ catch: function (e) {
486
+ var errorMsg = applyMessagePrefix(trimmedPrefix, "Caught: ".concat(e === null || e === void 0 ? void 0 : e.message, " ").concat(e === null || e === void 0 ? void 0 : e.stack));
487
+ base.error(errorMsg);
488
+ if (process.env.USE_SENTRY_LOGGING === "true") {
489
+ if (e instanceof Error) {
490
+ Sentry.captureException(e);
491
+ }
492
+ else if (Sentry.logger) {
493
+ Sentry.logger.error(errorMsg, mergeSentryLogAttributes(sentryExtras()));
494
+ }
495
+ }
496
+ },
497
+ debug: function (msg) {
498
+ var args = [];
499
+ for (var _i = 1; _i < arguments.length; _i++) {
500
+ args[_i - 1] = arguments[_i];
501
+ }
502
+ var line = applyMessagePrefix(trimmedPrefix, msg);
503
+ base.debug.apply(base, __spreadArray([line], __read(args), false));
504
+ sendToSentry(line, "debug", sentryExtras());
505
+ },
506
+ error: function (msg) {
507
+ var args = [];
508
+ for (var _i = 1; _i < arguments.length; _i++) {
509
+ args[_i - 1] = arguments[_i];
510
+ }
511
+ var line = applyMessagePrefix(trimmedPrefix, msg);
512
+ base.error.apply(base, __spreadArray([line], __read(args), false));
513
+ sendToSentry(line, "error", sentryExtras());
514
+ },
515
+ info: function (msg) {
516
+ var args = [];
517
+ for (var _i = 1; _i < arguments.length; _i++) {
518
+ args[_i - 1] = arguments[_i];
519
+ }
520
+ var line = applyMessagePrefix(trimmedPrefix, msg);
521
+ base.info.apply(base, __spreadArray([line], __read(args), false));
522
+ sendToSentry(line, "info", sentryExtras());
523
+ },
524
+ warn: function (msg) {
525
+ var args = [];
526
+ for (var _i = 1; _i < arguments.length; _i++) {
527
+ args[_i - 1] = arguments[_i];
528
+ }
529
+ var line = applyMessagePrefix(trimmedPrefix, msg);
530
+ base.warn.apply(base, __spreadArray([line], __read(args), false));
531
+ sendToSentry(line, "warn", sentryExtras());
532
+ },
533
+ };
534
+ };
535
+ exports.createScopedLogger = createScopedLogger;
536
+ /**
537
+ * Wraps a {@link ScopedLogger} so all `debug` / `info` / `warn` / `error` traffic is dropped while
538
+ * `isEnabled()` returns false. Use it to keep verbose diagnostics in the code but silent until a
539
+ * flag turns them on — no redeploy required.
540
+ *
541
+ * `isEnabled` is evaluated on **every** call, so it can read any feature-flag source: an
542
+ * environment variable, a cached/remote flag map, or a call into `@terreno/feature-flags` from your
543
+ * app. (`@terreno/api` deliberately does not import `@terreno/feature-flags` to avoid a package
544
+ * cycle — you supply the predicate.)
545
+ *
546
+ * @param options - The `isEnabled` predicate plus an optional `target` logger and `gateCatch`.
547
+ * @returns A scoped logger that forwards to `target` only while the flag is enabled.
548
+ * @see {@link createScopedLogger} for the usual `target`.
549
+ *
550
+ * @example Gate a scoped logger behind an env var (flips live, no restart)
551
+ * ```typescript
552
+ * import {createFeatureFlaggedLogger, createScopedLogger} from "@terreno/api";
553
+ *
554
+ * const jobLog = createFeatureFlaggedLogger({
555
+ * isEnabled: () => process.env.JOB_TRACE_LOGS === "true",
556
+ * target: createScopedLogger({prefix: "[Job]", labels: {jobName: "nightly-sync"}}),
557
+ * });
558
+ *
559
+ * jobLog.info("step 1"); // silent unless JOB_TRACE_LOGS=true
560
+ * ```
561
+ *
562
+ * @example Drive it from `@terreno/feature-flags` in app code
563
+ * ```typescript
564
+ * const debugLog = createFeatureFlaggedLogger({
565
+ * isEnabled: () => flags.isEnabled("debug.billing"),
566
+ * target: createScopedLogger({prefix: "[Billing]"}),
567
+ * gateCatch: true, // also silence `catch` while the flag is off (default: false)
568
+ * });
569
+ * ```
570
+ */
571
+ var createFeatureFlaggedLogger = function (options) {
572
+ var _a, _b;
573
+ var target = (_a = options.target) !== null && _a !== void 0 ? _a : exports.logger;
574
+ var gateCatch = (_b = options.gateCatch) !== null && _b !== void 0 ? _b : false;
575
+ return {
576
+ catch: function (e) {
577
+ if (gateCatch && !options.isEnabled()) {
578
+ return;
579
+ }
580
+ target.catch(e);
581
+ },
582
+ debug: function (msg) {
583
+ var args = [];
584
+ for (var _i = 1; _i < arguments.length; _i++) {
585
+ args[_i - 1] = arguments[_i];
586
+ }
587
+ if (!options.isEnabled()) {
588
+ return;
589
+ }
590
+ target.debug.apply(target, __spreadArray([msg], __read(args), false));
591
+ },
592
+ error: function (msg) {
593
+ var args = [];
594
+ for (var _i = 1; _i < arguments.length; _i++) {
595
+ args[_i - 1] = arguments[_i];
596
+ }
597
+ if (!options.isEnabled()) {
598
+ return;
599
+ }
600
+ target.error.apply(target, __spreadArray([msg], __read(args), false));
601
+ },
602
+ info: function (msg) {
603
+ var args = [];
604
+ for (var _i = 1; _i < arguments.length; _i++) {
605
+ args[_i - 1] = arguments[_i];
606
+ }
607
+ if (!options.isEnabled()) {
608
+ return;
609
+ }
610
+ target.info.apply(target, __spreadArray([msg], __read(args), false));
611
+ },
612
+ warn: function (msg) {
613
+ var args = [];
614
+ for (var _i = 1; _i < arguments.length; _i++) {
615
+ args[_i - 1] = arguments[_i];
616
+ }
617
+ if (!options.isEnabled()) {
618
+ return;
619
+ }
620
+ target.warn.apply(target, __spreadArray([msg], __read(args), false));
621
+ },
622
+ };
623
+ };
624
+ exports.createFeatureFlaggedLogger = createFeatureFlaggedLogger;
210
625
  var setupLogging = function (options) {
211
- var _a, e_1, _b;
626
+ var _a, e_4, _b;
212
627
  var _c, _d;
213
628
  exports.winstonLogger.clear();
629
+ terrenoDevJsonlAttached = false;
214
630
  if (!(options === null || options === void 0 ? void 0 : options.disableConsoleLogging)) {
215
631
  var formats = [addRequestContextFormat(), winston_1.default.format.simple()];
216
632
  if (!(options === null || options === void 0 ? void 0 : options.disableConsoleColors)) {
@@ -259,13 +675,16 @@ var setupLogging = function (options) {
259
675
  exports.winstonLogger.add(transport);
260
676
  }
261
677
  }
262
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
678
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
263
679
  finally {
264
680
  try {
265
681
  if (_f && !_f.done && (_b = _e.return)) _b.call(_e);
266
682
  }
267
- finally { if (e_1) throw e_1.error; }
683
+ finally { if (e_4) throw e_4.error; }
268
684
  }
269
685
  }
686
+ attachTerrenoDevJsonlTransportIfEnabled(exports.winstonLogger, {
687
+ disable: (options === null || options === void 0 ? void 0 : options.disableTerrenoDevJsonlLog) === true,
688
+ });
270
689
  };
271
690
  exports.setupLogging = setupLogging;