@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.
- package/.ai/guidelines/core.md +71 -0
- package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
- package/README.md +54 -1
- package/dist/__tests__/versionCheckPlugin.test.js +29 -7
- package/dist/actions.openApi.test.js +13 -11
- package/dist/api.js +98 -11
- package/dist/api.query.test.js +31 -1
- package/dist/api.test.js +211 -0
- package/dist/auth.test.js +10 -10
- package/dist/betterAuth.d.ts +1 -1
- package/dist/consentApp.test.js +1 -0
- package/dist/example.js +4 -4
- package/dist/expressServer.d.ts +0 -22
- package/dist/expressServer.js +1 -125
- package/dist/expressServer.test.js +90 -91
- package/dist/githubAuth.test.js +22 -22
- package/dist/logger.d.ts +154 -0
- package/dist/logger.js +445 -26
- package/dist/logger.test.js +435 -0
- package/dist/middleware.d.ts +7 -0
- package/dist/middleware.js +58 -1
- package/dist/middleware.test.js +159 -0
- package/dist/openApi.test.js +10 -17
- package/dist/openApiBuilder.test.js +18 -10
- package/dist/realtime/changeStreamWatcher.d.ts +4 -4
- package/dist/realtime/changeStreamWatcher.js +2 -4
- package/dist/realtime/queryMatcher.d.ts +1 -1
- package/dist/realtime/queryMatcher.js +39 -14
- package/dist/realtime/types.d.ts +3 -3
- package/dist/requestContext.d.ts +61 -0
- package/dist/requestContext.js +74 -0
- package/dist/secretProviders.test.js +335 -0
- package/dist/terrenoApp.d.ts +27 -15
- package/dist/terrenoApp.js +24 -14
- package/dist/terrenoApp.test.js +52 -0
- package/dist/tests/bunSetup.js +61 -7
- package/dist/tests.js +27 -4
- package/package.json +1 -1
- package/src/__tests__/versionCheckPlugin.test.ts +43 -15
- package/src/actions.openApi.test.ts +12 -10
- package/src/api.query.test.ts +24 -1
- package/src/api.test.ts +169 -0
- package/src/api.ts +71 -0
- package/src/auth.test.ts +10 -10
- package/src/betterAuth.ts +1 -1
- package/src/consentApp.test.ts +1 -0
- package/src/example.ts +4 -4
- package/src/expressServer.test.ts +82 -85
- package/src/expressServer.ts +1 -213
- package/src/githubAuth.test.ts +22 -22
- package/src/logger.test.ts +466 -1
- package/src/logger.ts +477 -14
- package/src/middleware.test.ts +74 -2
- package/src/middleware.ts +57 -0
- package/src/openApi.test.ts +10 -17
- package/src/openApiBuilder.test.ts +18 -10
- package/src/realtime/changeStreamWatcher.ts +15 -10
- package/src/realtime/queryMatcher.ts +54 -27
- package/src/realtime/types.ts +4 -4
- package/src/requestContext.ts +86 -0
- package/src/secretProviders.test.ts +219 -1
- package/src/terrenoApp.test.ts +38 -0
- package/src/terrenoApp.ts +37 -15
- package/src/tests/bunSetup.ts +16 -3
- package/src/tests.ts +17 -4
package/dist/requestContext.js
CHANGED
|
@@ -72,6 +72,31 @@ var __values = (this && this.__values) || function(o) {
|
|
|
72
72
|
};
|
|
73
73
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74
74
|
exports.requestContextMiddleware = exports.updateRequestContextFromRequest = exports.runWithRequestContextAttributes = exports.runWithRequestContext = exports.getCurrentRequestContextAttributes = exports.setRequestContext = exports.applyRequestContextToSentry = exports.getCurrentLogContext = exports.getCurrentRequestContext = exports.getRequestContextFromAttributes = exports.getSessionIdFromJwtPayload = exports.REQUEST_CONTEXT_ATTRIBUTE_NAMES = void 0;
|
|
75
|
+
/**
|
|
76
|
+
* Request/job correlation for `@terreno/api`.
|
|
77
|
+
*
|
|
78
|
+
* Correlation is how every log line emitted while handling one request (or one background job) can
|
|
79
|
+
* be tied back together. It is built on Node's {@link AsyncLocalStorage}: a {@link RequestContext}
|
|
80
|
+
* (with `requestId`, `userId`, `traceId`, etc.) is stored for the duration of a callback, and the
|
|
81
|
+
* logger's Winston format reads it from there and merges it into each line. Nothing needs to be
|
|
82
|
+
* threaded through function arguments.
|
|
83
|
+
*
|
|
84
|
+
* Two ways a scope is established:
|
|
85
|
+
*
|
|
86
|
+
* - **HTTP**: {@link requestContextMiddleware} runs first in the middleware stack. It derives a
|
|
87
|
+
* `requestId` from incoming headers ({@link REQUEST_CONTEXT_ATTRIBUTE_NAMES}, `x-correlation-id`,
|
|
88
|
+
* Cloud Trace, or W3C `traceparent`) or generates one, echoes it back as `X-Request-ID`, and runs
|
|
89
|
+
* the rest of the request inside the scope.
|
|
90
|
+
* - **Jobs/scripts**: {@link runWithRequestContext} (or {@link runWithRequestContextAttributes})
|
|
91
|
+
* establishes the same scope manually so background work is just as traceable.
|
|
92
|
+
*
|
|
93
|
+
* The active context is also pushed to Sentry tags/context via {@link applyRequestContextToSentry},
|
|
94
|
+
* and is exposed to logging via {@link getCurrentLogContext} / {@link getCurrentRequestContext}.
|
|
95
|
+
*
|
|
96
|
+
* @see {@link runWithRequestContext}
|
|
97
|
+
* @see {@link getCurrentLogContext}
|
|
98
|
+
* @module requestContext
|
|
99
|
+
*/
|
|
75
100
|
var node_async_hooks_1 = require("node:async_hooks");
|
|
76
101
|
var node_crypto_1 = require("node:crypto");
|
|
77
102
|
var Sentry = __importStar(require("@sentry/bun"));
|
|
@@ -84,6 +109,11 @@ var TRACE_ID_HEADER = "x-trace-id";
|
|
|
84
109
|
var TRACE_PARENT_HEADER = "traceparent";
|
|
85
110
|
var TRACE_SAMPLED_HEADER = "x-trace-sampled";
|
|
86
111
|
var USER_ID_HEADER = "x-user-id";
|
|
112
|
+
/**
|
|
113
|
+
* Canonical HTTP header names for each correlation field. Use these to propagate context to
|
|
114
|
+
* downstream services (pair with {@link getCurrentRequestContextAttributes}) or to read it from an
|
|
115
|
+
* incoming request (pair with {@link getRequestContextFromAttributes}).
|
|
116
|
+
*/
|
|
87
117
|
exports.REQUEST_CONTEXT_ATTRIBUTE_NAMES = {
|
|
88
118
|
jobId: JOB_ID_HEADER,
|
|
89
119
|
requestId: "x-request-id",
|
|
@@ -193,10 +223,19 @@ var getRequestContextFromAttributes = function (attributes) {
|
|
|
193
223
|
};
|
|
194
224
|
};
|
|
195
225
|
exports.getRequestContextFromAttributes = getRequestContextFromAttributes;
|
|
226
|
+
/**
|
|
227
|
+
* Returns the full {@link RequestContext} for the active AsyncLocalStorage scope, or `undefined`
|
|
228
|
+
* when called outside any request/job scope. The logger uses this to enrich each line.
|
|
229
|
+
*/
|
|
196
230
|
var getCurrentRequestContext = function () {
|
|
197
231
|
return requestContextStorage.getStore();
|
|
198
232
|
};
|
|
199
233
|
exports.getCurrentRequestContext = getCurrentRequestContext;
|
|
234
|
+
/**
|
|
235
|
+
* Returns the active correlation fields as a plain object (empty when outside a scope). This is the
|
|
236
|
+
* shape attached to Sentry log attributes and is handy when you need to log or forward the current
|
|
237
|
+
* context yourself.
|
|
238
|
+
*/
|
|
200
239
|
var getCurrentLogContext = function () {
|
|
201
240
|
var context = (0, exports.getCurrentRequestContext)();
|
|
202
241
|
if (!context) {
|
|
@@ -266,6 +305,11 @@ var setAttribute = function (attributes, name, value) {
|
|
|
266
305
|
}
|
|
267
306
|
attributes[name] = String(value);
|
|
268
307
|
};
|
|
308
|
+
/**
|
|
309
|
+
* Serializes the active correlation context into HTTP header attributes (keyed by
|
|
310
|
+
* {@link REQUEST_CONTEXT_ATTRIBUTE_NAMES}) so it can be propagated on outbound requests to other
|
|
311
|
+
* services, keeping the same `requestId`/`traceId` across service boundaries.
|
|
312
|
+
*/
|
|
269
313
|
var getCurrentRequestContextAttributes = function (overrides) {
|
|
270
314
|
if (overrides === void 0) { overrides = {}; }
|
|
271
315
|
var context = __assign(__assign({}, (0, exports.getCurrentLogContext)()), overrides);
|
|
@@ -280,6 +324,23 @@ var getCurrentRequestContextAttributes = function (overrides) {
|
|
|
280
324
|
return attributes;
|
|
281
325
|
};
|
|
282
326
|
exports.getCurrentRequestContextAttributes = getCurrentRequestContextAttributes;
|
|
327
|
+
/**
|
|
328
|
+
* Runs `callback` inside a fresh correlation scope so every log line it emits shares the same
|
|
329
|
+
* identifiers — the manual equivalent of {@link requestContextMiddleware} for background jobs,
|
|
330
|
+
* cron tasks, scripts, queue consumers, etc. A `requestId` is generated when not supplied, and the
|
|
331
|
+
* context is mirrored to Sentry.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```typescript
|
|
335
|
+
* import {createScopedLogger, runWithRequestContext} from "@terreno/api";
|
|
336
|
+
*
|
|
337
|
+
* await runWithRequestContext({jobId: "nightly-sync"}, async () => {
|
|
338
|
+
* const log = createScopedLogger({prefix: "[NightlySync]"});
|
|
339
|
+
* log.info("started"); // includes jobId + a generated requestId on every line
|
|
340
|
+
* await sync();
|
|
341
|
+
* });
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
283
344
|
var runWithRequestContext = function (context, callback) {
|
|
284
345
|
var _a;
|
|
285
346
|
var nextContext = __assign(__assign({}, context), { requestId: (_a = context.requestId) !== null && _a !== void 0 ? _a : (0, node_crypto_1.randomUUID)() });
|
|
@@ -289,6 +350,11 @@ var runWithRequestContext = function (context, callback) {
|
|
|
289
350
|
});
|
|
290
351
|
};
|
|
291
352
|
exports.runWithRequestContext = runWithRequestContext;
|
|
353
|
+
/**
|
|
354
|
+
* Like {@link runWithRequestContext}, but seeds the scope from raw header attributes (for example
|
|
355
|
+
* those received on an incoming message or forwarded by another service). Parses Cloud Trace / W3C
|
|
356
|
+
* `traceparent` into `traceId`/`spanId` via {@link getRequestContextFromAttributes}.
|
|
357
|
+
*/
|
|
292
358
|
var runWithRequestContextAttributes = function (attributes, callback) {
|
|
293
359
|
if (attributes === void 0) { attributes = {}; }
|
|
294
360
|
return (0, exports.runWithRequestContext)((0, exports.getRequestContextFromAttributes)(attributes), callback);
|
|
@@ -312,6 +378,14 @@ var updateRequestContextFromRequest = function (req, res) {
|
|
|
312
378
|
}
|
|
313
379
|
};
|
|
314
380
|
exports.updateRequestContextFromRequest = updateRequestContextFromRequest;
|
|
381
|
+
/**
|
|
382
|
+
* Express middleware that opens a correlation scope for the request. Mounted early by `TerrenoApp` /
|
|
383
|
+
* `setupServer`, it resolves a `requestId` (from request-id/correlation headers, Cloud Trace, or
|
|
384
|
+
* W3C `traceparent`, else a new UUID), captures any `jobId`/`sessionId`/trace fields, echoes
|
|
385
|
+
* `X-Request-ID` back to the client, and runs the remaining middleware inside the scope so all
|
|
386
|
+
* downstream logs are correlated. A later auth-aware pass ({@link updateRequestContextFromRequest})
|
|
387
|
+
* fills in `userId`/`sessionId`.
|
|
388
|
+
*/
|
|
315
389
|
var requestContextMiddleware = function (req, res, next) {
|
|
316
390
|
var cloudTraceContext = parseGoogleCloudTraceContext(getHeader(req, CLOUD_TRACE_CONTEXT_HEADER));
|
|
317
391
|
var traceParentContext = parseTraceParent(getHeader(req, TRACE_PARENT_HEADER));
|
|
@@ -37,6 +37,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
var bun_test_1 = require("bun:test");
|
|
40
|
+
var errors_1 = require("./errors");
|
|
40
41
|
var secretProviders_1 = require("./secretProviders");
|
|
41
42
|
(0, bun_test_1.describe)("EnvSecretProvider", function () {
|
|
42
43
|
(0, bun_test_1.beforeEach)(function () {
|
|
@@ -389,3 +390,337 @@ var secretProviders_1 = require("./secretProviders");
|
|
|
389
390
|
});
|
|
390
391
|
}); });
|
|
391
392
|
});
|
|
393
|
+
/** Inject a pre-built mock client into a GcpSecretProvider, bypassing getClient(). */
|
|
394
|
+
var injectClient = function (provider, client) {
|
|
395
|
+
// Bypass the private `client` field for testing — avoids the dynamic import of
|
|
396
|
+
// @google-cloud/secret-manager which is an optional peer dependency.
|
|
397
|
+
Object.defineProperty(provider, "client", { configurable: true, value: client, writable: true });
|
|
398
|
+
};
|
|
399
|
+
(0, bun_test_1.describe)("GcpSecretProvider", function () {
|
|
400
|
+
(0, bun_test_1.it)("has the name 'gcp'", function () {
|
|
401
|
+
var provider = new secretProviders_1.GcpSecretProvider({ projectId: "my-project" });
|
|
402
|
+
(0, bun_test_1.expect)(provider.name).toBe("gcp");
|
|
403
|
+
});
|
|
404
|
+
(0, bun_test_1.it)("throws APIError when @google-cloud/secret-manager is not installed", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
405
|
+
var provider, error_1;
|
|
406
|
+
return __generator(this, function (_a) {
|
|
407
|
+
switch (_a.label) {
|
|
408
|
+
case 0:
|
|
409
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "my-project" });
|
|
410
|
+
_a.label = 1;
|
|
411
|
+
case 1:
|
|
412
|
+
_a.trys.push([1, 3, , 4]);
|
|
413
|
+
return [4 /*yield*/, provider.getSecret("some-secret")];
|
|
414
|
+
case 2:
|
|
415
|
+
_a.sent();
|
|
416
|
+
bun_test_1.expect.unreachable("should have thrown");
|
|
417
|
+
return [3 /*break*/, 4];
|
|
418
|
+
case 3:
|
|
419
|
+
error_1 = _a.sent();
|
|
420
|
+
(0, bun_test_1.expect)(error_1).toBeInstanceOf(errors_1.APIError);
|
|
421
|
+
(0, bun_test_1.expect)(error_1.title).toContain("GcpSecretProvider requires @google-cloud/secret-manager");
|
|
422
|
+
return [3 /*break*/, 4];
|
|
423
|
+
case 4: return [2 /*return*/];
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}); });
|
|
427
|
+
(0, bun_test_1.it)("resolves a short secret name to the full resource path with default version", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
428
|
+
var calls, mockClient, provider, result;
|
|
429
|
+
return __generator(this, function (_a) {
|
|
430
|
+
switch (_a.label) {
|
|
431
|
+
case 0:
|
|
432
|
+
calls = [];
|
|
433
|
+
mockClient = {
|
|
434
|
+
accessSecretVersion: function (req) { return __awaiter(void 0, void 0, void 0, function () {
|
|
435
|
+
return __generator(this, function (_a) {
|
|
436
|
+
calls.push(req.name);
|
|
437
|
+
return [2 /*return*/, [{ payload: { data: "secret-value" } }]];
|
|
438
|
+
});
|
|
439
|
+
}); },
|
|
440
|
+
};
|
|
441
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "my-project" });
|
|
442
|
+
injectClient(provider, mockClient);
|
|
443
|
+
return [4 /*yield*/, provider.getSecret("openai-api-key")];
|
|
444
|
+
case 1:
|
|
445
|
+
result = _a.sent();
|
|
446
|
+
(0, bun_test_1.expect)(result).toBe("secret-value");
|
|
447
|
+
(0, bun_test_1.expect)(calls).toEqual(["projects/my-project/secrets/openai-api-key/versions/latest"]);
|
|
448
|
+
return [2 /*return*/];
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}); });
|
|
452
|
+
(0, bun_test_1.it)("resolves a short secret name with an explicit version", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
453
|
+
var calls, mockClient, provider, result;
|
|
454
|
+
return __generator(this, function (_a) {
|
|
455
|
+
switch (_a.label) {
|
|
456
|
+
case 0:
|
|
457
|
+
calls = [];
|
|
458
|
+
mockClient = {
|
|
459
|
+
accessSecretVersion: function (req) { return __awaiter(void 0, void 0, void 0, function () {
|
|
460
|
+
return __generator(this, function (_a) {
|
|
461
|
+
calls.push(req.name);
|
|
462
|
+
return [2 /*return*/, [{ payload: { data: "v3-value" } }]];
|
|
463
|
+
});
|
|
464
|
+
}); },
|
|
465
|
+
};
|
|
466
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "p" });
|
|
467
|
+
injectClient(provider, mockClient);
|
|
468
|
+
return [4 /*yield*/, provider.getSecret("my-key", "3")];
|
|
469
|
+
case 1:
|
|
470
|
+
result = _a.sent();
|
|
471
|
+
(0, bun_test_1.expect)(result).toBe("v3-value");
|
|
472
|
+
(0, bun_test_1.expect)(calls).toEqual(["projects/p/secrets/my-key/versions/3"]);
|
|
473
|
+
return [2 /*return*/];
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}); });
|
|
477
|
+
(0, bun_test_1.it)("honors a full resource path that already contains /versions/", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
478
|
+
var calls, mockClient, provider, result;
|
|
479
|
+
return __generator(this, function (_a) {
|
|
480
|
+
switch (_a.label) {
|
|
481
|
+
case 0:
|
|
482
|
+
calls = [];
|
|
483
|
+
mockClient = {
|
|
484
|
+
accessSecretVersion: function (req) { return __awaiter(void 0, void 0, void 0, function () {
|
|
485
|
+
return __generator(this, function (_a) {
|
|
486
|
+
calls.push(req.name);
|
|
487
|
+
return [2 /*return*/, [{ payload: { data: "pinned" } }]];
|
|
488
|
+
});
|
|
489
|
+
}); },
|
|
490
|
+
};
|
|
491
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "ignored" });
|
|
492
|
+
injectClient(provider, mockClient);
|
|
493
|
+
return [4 /*yield*/, provider.getSecret("projects/p/secrets/s/versions/7")];
|
|
494
|
+
case 1:
|
|
495
|
+
result = _a.sent();
|
|
496
|
+
(0, bun_test_1.expect)(result).toBe("pinned");
|
|
497
|
+
(0, bun_test_1.expect)(calls).toEqual(["projects/p/secrets/s/versions/7"]);
|
|
498
|
+
return [2 /*return*/];
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}); });
|
|
502
|
+
(0, bun_test_1.it)("appends /versions/latest to a full resource path without a version suffix", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
503
|
+
var calls, mockClient, provider, result;
|
|
504
|
+
return __generator(this, function (_a) {
|
|
505
|
+
switch (_a.label) {
|
|
506
|
+
case 0:
|
|
507
|
+
calls = [];
|
|
508
|
+
mockClient = {
|
|
509
|
+
accessSecretVersion: function (req) { return __awaiter(void 0, void 0, void 0, function () {
|
|
510
|
+
return __generator(this, function (_a) {
|
|
511
|
+
calls.push(req.name);
|
|
512
|
+
return [2 /*return*/, [{ payload: { data: "latest-value" } }]];
|
|
513
|
+
});
|
|
514
|
+
}); },
|
|
515
|
+
};
|
|
516
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "ignored" });
|
|
517
|
+
injectClient(provider, mockClient);
|
|
518
|
+
return [4 /*yield*/, provider.getSecret("projects/p/secrets/s")];
|
|
519
|
+
case 1:
|
|
520
|
+
result = _a.sent();
|
|
521
|
+
(0, bun_test_1.expect)(result).toBe("latest-value");
|
|
522
|
+
(0, bun_test_1.expect)(calls).toEqual(["projects/p/secrets/s/versions/latest"]);
|
|
523
|
+
return [2 /*return*/];
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}); });
|
|
527
|
+
(0, bun_test_1.it)("appends the explicit version when full path lacks /versions/", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
528
|
+
var calls, mockClient, provider, result;
|
|
529
|
+
return __generator(this, function (_a) {
|
|
530
|
+
switch (_a.label) {
|
|
531
|
+
case 0:
|
|
532
|
+
calls = [];
|
|
533
|
+
mockClient = {
|
|
534
|
+
accessSecretVersion: function (req) { return __awaiter(void 0, void 0, void 0, function () {
|
|
535
|
+
return __generator(this, function (_a) {
|
|
536
|
+
calls.push(req.name);
|
|
537
|
+
return [2 /*return*/, [{ payload: { data: "v5" } }]];
|
|
538
|
+
});
|
|
539
|
+
}); },
|
|
540
|
+
};
|
|
541
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "ignored" });
|
|
542
|
+
injectClient(provider, mockClient);
|
|
543
|
+
return [4 /*yield*/, provider.getSecret("projects/p/secrets/s", "5")];
|
|
544
|
+
case 1:
|
|
545
|
+
result = _a.sent();
|
|
546
|
+
(0, bun_test_1.expect)(result).toBe("v5");
|
|
547
|
+
(0, bun_test_1.expect)(calls).toEqual(["projects/p/secrets/s/versions/5"]);
|
|
548
|
+
return [2 /*return*/];
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}); });
|
|
552
|
+
(0, bun_test_1.it)("decodes a Uint8Array payload", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
553
|
+
var encoded, mockClient, provider, _a;
|
|
554
|
+
return __generator(this, function (_b) {
|
|
555
|
+
switch (_b.label) {
|
|
556
|
+
case 0:
|
|
557
|
+
encoded = new TextEncoder().encode("binary-secret");
|
|
558
|
+
mockClient = {
|
|
559
|
+
accessSecretVersion: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
560
|
+
return [2 /*return*/, [{ payload: { data: encoded } }]];
|
|
561
|
+
}); }); },
|
|
562
|
+
};
|
|
563
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "p" });
|
|
564
|
+
injectClient(provider, mockClient);
|
|
565
|
+
_a = bun_test_1.expect;
|
|
566
|
+
return [4 /*yield*/, provider.getSecret("bin-key")];
|
|
567
|
+
case 1:
|
|
568
|
+
_a.apply(void 0, [_b.sent()]).toBe("binary-secret");
|
|
569
|
+
return [2 /*return*/];
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}); });
|
|
573
|
+
(0, bun_test_1.it)("returns null when the payload is empty", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
574
|
+
var mockClient, provider, _a;
|
|
575
|
+
return __generator(this, function (_b) {
|
|
576
|
+
switch (_b.label) {
|
|
577
|
+
case 0:
|
|
578
|
+
mockClient = {
|
|
579
|
+
accessSecretVersion: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
580
|
+
return [2 /*return*/, [{ payload: {} }]];
|
|
581
|
+
}); }); },
|
|
582
|
+
};
|
|
583
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "p" });
|
|
584
|
+
injectClient(provider, mockClient);
|
|
585
|
+
_a = bun_test_1.expect;
|
|
586
|
+
return [4 /*yield*/, provider.getSecret("empty-payload")];
|
|
587
|
+
case 1:
|
|
588
|
+
_a.apply(void 0, [_b.sent()]).toBeNull();
|
|
589
|
+
return [2 /*return*/];
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}); });
|
|
593
|
+
(0, bun_test_1.it)("returns null when the payload field is missing entirely", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
594
|
+
var mockClient, provider, _a;
|
|
595
|
+
return __generator(this, function (_b) {
|
|
596
|
+
switch (_b.label) {
|
|
597
|
+
case 0:
|
|
598
|
+
mockClient = {
|
|
599
|
+
accessSecretVersion: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
600
|
+
return [2 /*return*/, [{}]];
|
|
601
|
+
}); }); },
|
|
602
|
+
};
|
|
603
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "p" });
|
|
604
|
+
injectClient(provider, mockClient);
|
|
605
|
+
_a = bun_test_1.expect;
|
|
606
|
+
return [4 /*yield*/, provider.getSecret("no-payload")];
|
|
607
|
+
case 1:
|
|
608
|
+
_a.apply(void 0, [_b.sent()]).toBeNull();
|
|
609
|
+
return [2 /*return*/];
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}); });
|
|
613
|
+
(0, bun_test_1.it)("returns null on NOT_FOUND (gRPC code 5)", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
614
|
+
var notFound, mockClient, provider, _a;
|
|
615
|
+
return __generator(this, function (_b) {
|
|
616
|
+
switch (_b.label) {
|
|
617
|
+
case 0:
|
|
618
|
+
notFound = Object.assign(new Error("NOT_FOUND"), { code: 5 });
|
|
619
|
+
mockClient = {
|
|
620
|
+
accessSecretVersion: function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
621
|
+
return __generator(this, function (_a) {
|
|
622
|
+
throw notFound;
|
|
623
|
+
});
|
|
624
|
+
}); },
|
|
625
|
+
};
|
|
626
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "p" });
|
|
627
|
+
injectClient(provider, mockClient);
|
|
628
|
+
_a = bun_test_1.expect;
|
|
629
|
+
return [4 /*yield*/, provider.getSecret("missing-secret")];
|
|
630
|
+
case 1:
|
|
631
|
+
_a.apply(void 0, [_b.sent()]).toBeNull();
|
|
632
|
+
return [2 /*return*/];
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}); });
|
|
636
|
+
(0, bun_test_1.it)("re-throws non-NOT_FOUND errors", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
637
|
+
var permissionDenied, mockClient, provider, error_2;
|
|
638
|
+
return __generator(this, function (_a) {
|
|
639
|
+
switch (_a.label) {
|
|
640
|
+
case 0:
|
|
641
|
+
permissionDenied = Object.assign(new Error("PERMISSION_DENIED"), { code: 7 });
|
|
642
|
+
mockClient = {
|
|
643
|
+
accessSecretVersion: function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
644
|
+
return __generator(this, function (_a) {
|
|
645
|
+
throw permissionDenied;
|
|
646
|
+
});
|
|
647
|
+
}); },
|
|
648
|
+
};
|
|
649
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "p" });
|
|
650
|
+
injectClient(provider, mockClient);
|
|
651
|
+
_a.label = 1;
|
|
652
|
+
case 1:
|
|
653
|
+
_a.trys.push([1, 3, , 4]);
|
|
654
|
+
return [4 /*yield*/, provider.getSecret("forbidden-secret")];
|
|
655
|
+
case 2:
|
|
656
|
+
_a.sent();
|
|
657
|
+
bun_test_1.expect.unreachable("should have thrown");
|
|
658
|
+
return [3 /*break*/, 4];
|
|
659
|
+
case 3:
|
|
660
|
+
error_2 = _a.sent();
|
|
661
|
+
(0, bun_test_1.expect)(error_2).toBe(permissionDenied);
|
|
662
|
+
return [3 /*break*/, 4];
|
|
663
|
+
case 4: return [2 /*return*/];
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}); });
|
|
667
|
+
(0, bun_test_1.it)("re-throws non-Error throwables", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
668
|
+
var mockClient, provider, error_3;
|
|
669
|
+
return __generator(this, function (_a) {
|
|
670
|
+
switch (_a.label) {
|
|
671
|
+
case 0:
|
|
672
|
+
mockClient = {
|
|
673
|
+
accessSecretVersion: function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
674
|
+
return __generator(this, function (_a) {
|
|
675
|
+
throw "string-error";
|
|
676
|
+
});
|
|
677
|
+
}); },
|
|
678
|
+
};
|
|
679
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "p" });
|
|
680
|
+
injectClient(provider, mockClient);
|
|
681
|
+
_a.label = 1;
|
|
682
|
+
case 1:
|
|
683
|
+
_a.trys.push([1, 3, , 4]);
|
|
684
|
+
return [4 /*yield*/, provider.getSecret("x")];
|
|
685
|
+
case 2:
|
|
686
|
+
_a.sent();
|
|
687
|
+
bun_test_1.expect.unreachable("should have thrown");
|
|
688
|
+
return [3 /*break*/, 4];
|
|
689
|
+
case 3:
|
|
690
|
+
error_3 = _a.sent();
|
|
691
|
+
(0, bun_test_1.expect)(error_3).toBe("string-error");
|
|
692
|
+
return [3 /*break*/, 4];
|
|
693
|
+
case 4: return [2 /*return*/];
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}); });
|
|
697
|
+
(0, bun_test_1.it)("caches the client across multiple getSecret calls", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
698
|
+
var callCount, mockClient, provider, _a, _b;
|
|
699
|
+
return __generator(this, function (_c) {
|
|
700
|
+
switch (_c.label) {
|
|
701
|
+
case 0:
|
|
702
|
+
callCount = 0;
|
|
703
|
+
mockClient = {
|
|
704
|
+
accessSecretVersion: function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
705
|
+
return __generator(this, function (_a) {
|
|
706
|
+
callCount++;
|
|
707
|
+
return [2 /*return*/, [{ payload: { data: "call-".concat(callCount) } }]];
|
|
708
|
+
});
|
|
709
|
+
}); },
|
|
710
|
+
};
|
|
711
|
+
provider = new secretProviders_1.GcpSecretProvider({ projectId: "p" });
|
|
712
|
+
injectClient(provider, mockClient);
|
|
713
|
+
_a = bun_test_1.expect;
|
|
714
|
+
return [4 /*yield*/, provider.getSecret("a")];
|
|
715
|
+
case 1:
|
|
716
|
+
_a.apply(void 0, [_c.sent()]).toBe("call-1");
|
|
717
|
+
_b = bun_test_1.expect;
|
|
718
|
+
return [4 /*yield*/, provider.getSecret("b")];
|
|
719
|
+
case 2:
|
|
720
|
+
_b.apply(void 0, [_c.sent()]).toBe("call-2");
|
|
721
|
+
(0, bun_test_1.expect)(callCount).toBe(2);
|
|
722
|
+
return [2 /*return*/];
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
}); });
|
|
726
|
+
});
|
package/dist/terrenoApp.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import express from "express";
|
|
|
3
3
|
import type { ModelRouterRegistration } from "./api";
|
|
4
4
|
import { type UserModel as UserMongooseModel } from "./auth";
|
|
5
5
|
import { type ConfigurationAppOptions } from "./configurationApp";
|
|
6
|
-
import { type AuthOptions } from "./expressServer";
|
|
6
|
+
import { type AddRoutes, type AuthOptions } from "./expressServer";
|
|
7
7
|
import { type GitHubAuthOptions } from "./githubAuth";
|
|
8
8
|
import { type LoggingOptions } from "./logger";
|
|
9
9
|
import type { RealtimeAppOptions } from "./realtime/types";
|
|
@@ -38,27 +38,40 @@ export interface TerrenoAppOptions {
|
|
|
38
38
|
* Set to `true` for defaults, or pass a RealtimeAppOptions object for full control.
|
|
39
39
|
*/
|
|
40
40
|
realtime?: boolean | RealtimeAppOptions;
|
|
41
|
+
/**
|
|
42
|
+
* Runs after CORS and before the `addMiddleware` chain and JSON body parsing.
|
|
43
|
+
* Use to attach early middleware via `app.use(...)` before JSON parsing.
|
|
44
|
+
*/
|
|
45
|
+
beforeJsonSetup?: (app: express.Application) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Invoked after registered plugins/model routers and before `/auth/me`.
|
|
48
|
+
* Receives the Express app and OpenAPI bundle for `modelRouter` / `createOpenApiBuilder` wiring.
|
|
49
|
+
*/
|
|
50
|
+
configureApp?: AddRoutes;
|
|
41
51
|
}
|
|
42
52
|
/**
|
|
43
53
|
* Fluent API for building Express applications with Terreno framework.
|
|
44
54
|
*
|
|
45
|
-
* TerrenoApp
|
|
46
|
-
*
|
|
47
|
-
*
|
|
55
|
+
* TerrenoApp is the supported way to assemble the Terreno Express stack.
|
|
56
|
+
* Build applications by registering model routers and plugins (and/or
|
|
57
|
+
* `configureApp`), then calling `start()` to listen.
|
|
48
58
|
*
|
|
49
59
|
* The middleware stack is configured in this order:
|
|
50
60
|
* 1. CORS
|
|
51
|
-
* 2.
|
|
52
|
-
* 3.
|
|
53
|
-
* 4.
|
|
54
|
-
* 5.
|
|
55
|
-
* 6.
|
|
56
|
-
* 7.
|
|
57
|
-
* 8.
|
|
58
|
-
* 9.
|
|
61
|
+
* 2. Optional `beforeJsonSetup` (configure the app before JSON parsing)
|
|
62
|
+
* 3. Custom middleware (via addMiddleware)
|
|
63
|
+
* 4. JSON body parser
|
|
64
|
+
* 5. Auth routes (/auth/login, /auth/signup, etc.)
|
|
65
|
+
* 6. JWT authentication setup
|
|
66
|
+
* 7. Request logging
|
|
67
|
+
* 8. Sentry scopes
|
|
68
|
+
* 9. OpenAPI middleware (including JSON `requestId` on object responses)
|
|
59
69
|
* 10. GitHub OAuth routes (if enabled)
|
|
60
|
-
* 11.
|
|
61
|
-
* 12.
|
|
70
|
+
* 11. Configuration app (if any)
|
|
71
|
+
* 12. Registered model routers and plugins
|
|
72
|
+
* 13. Optional `configureApp` callback
|
|
73
|
+
* 14. /auth/me routes
|
|
74
|
+
* 15. Error handling middleware
|
|
62
75
|
*
|
|
63
76
|
* @example
|
|
64
77
|
* ```typescript
|
|
@@ -95,7 +108,6 @@ export interface TerrenoAppOptions {
|
|
|
95
108
|
* .start();
|
|
96
109
|
* ```
|
|
97
110
|
*
|
|
98
|
-
* @see setupServer for the callback-based alternative
|
|
99
111
|
* @see TerrenoPlugin for creating reusable plugins
|
|
100
112
|
* @see modelRouter for creating CRUD route registrations
|
|
101
113
|
*/
|
package/dist/terrenoApp.js
CHANGED
|
@@ -70,6 +70,7 @@ var errors_1 = require("./errors");
|
|
|
70
70
|
var expressServer_1 = require("./expressServer");
|
|
71
71
|
var githubAuth_1 = require("./githubAuth");
|
|
72
72
|
var logger_1 = require("./logger");
|
|
73
|
+
var middleware_1 = require("./middleware");
|
|
73
74
|
var openApiCompat_1 = require("./openApiCompat");
|
|
74
75
|
var openApiEtag_1 = require("./openApiEtag");
|
|
75
76
|
var realtimeApp_1 = require("./realtime/realtimeApp");
|
|
@@ -78,23 +79,26 @@ var index_1 = __importDefault(require("./vendor/wesleytodd-openapi/index"));
|
|
|
78
79
|
/**
|
|
79
80
|
* Fluent API for building Express applications with Terreno framework.
|
|
80
81
|
*
|
|
81
|
-
* TerrenoApp
|
|
82
|
-
*
|
|
83
|
-
*
|
|
82
|
+
* TerrenoApp is the supported way to assemble the Terreno Express stack.
|
|
83
|
+
* Build applications by registering model routers and plugins (and/or
|
|
84
|
+
* `configureApp`), then calling `start()` to listen.
|
|
84
85
|
*
|
|
85
86
|
* The middleware stack is configured in this order:
|
|
86
87
|
* 1. CORS
|
|
87
|
-
* 2.
|
|
88
|
-
* 3.
|
|
89
|
-
* 4.
|
|
90
|
-
* 5.
|
|
91
|
-
* 6.
|
|
92
|
-
* 7.
|
|
93
|
-
* 8.
|
|
94
|
-
* 9.
|
|
88
|
+
* 2. Optional `beforeJsonSetup` (configure the app before JSON parsing)
|
|
89
|
+
* 3. Custom middleware (via addMiddleware)
|
|
90
|
+
* 4. JSON body parser
|
|
91
|
+
* 5. Auth routes (/auth/login, /auth/signup, etc.)
|
|
92
|
+
* 6. JWT authentication setup
|
|
93
|
+
* 7. Request logging
|
|
94
|
+
* 8. Sentry scopes
|
|
95
|
+
* 9. OpenAPI middleware (including JSON `requestId` on object responses)
|
|
95
96
|
* 10. GitHub OAuth routes (if enabled)
|
|
96
|
-
* 11.
|
|
97
|
-
* 12.
|
|
97
|
+
* 11. Configuration app (if any)
|
|
98
|
+
* 12. Registered model routers and plugins
|
|
99
|
+
* 13. Optional `configureApp` callback
|
|
100
|
+
* 14. /auth/me routes
|
|
101
|
+
* 15. Error handling middleware
|
|
98
102
|
*
|
|
99
103
|
* @example
|
|
100
104
|
* ```typescript
|
|
@@ -131,7 +135,6 @@ var index_1 = __importDefault(require("./vendor/wesleytodd-openapi/index"));
|
|
|
131
135
|
* .start();
|
|
132
136
|
* ```
|
|
133
137
|
*
|
|
134
|
-
* @see setupServer for the callback-based alternative
|
|
135
138
|
* @see TerrenoPlugin for creating reusable plugins
|
|
136
139
|
* @see modelRouter for creating CRUD route registrations
|
|
137
140
|
*/
|
|
@@ -254,6 +257,9 @@ var TerrenoApp = /** @class */ (function () {
|
|
|
254
257
|
app.set("query parser", function (str) { var _a; return qs_1.default.parse(str, { arrayLimit: (_a = options.arrayLimit) !== null && _a !== void 0 ? _a : 200 }); });
|
|
255
258
|
app.use(requestContext_1.requestContextMiddleware);
|
|
256
259
|
app.use((0, cors_1.default)({ credentials: true, origin: (_c = options.corsOrigin) !== null && _c !== void 0 ? _c : "*" }));
|
|
260
|
+
if (options.beforeJsonSetup) {
|
|
261
|
+
options.beforeJsonSetup(app);
|
|
262
|
+
}
|
|
257
263
|
try {
|
|
258
264
|
// Apply custom middleware before JSON parsing
|
|
259
265
|
for (var _d = __values(this.middlewareFns), _e = _d.next(); !_e.done; _e = _d.next()) {
|
|
@@ -314,6 +320,7 @@ var TerrenoApp = /** @class */ (function () {
|
|
|
314
320
|
// OpenAPI
|
|
315
321
|
app.use(openApiCompat_1.openApiCompatMiddleware);
|
|
316
322
|
app.use(openApiEtag_1.openApiEtagMiddleware);
|
|
323
|
+
app.use(middleware_1.jsonResponseRequestIdMiddleware);
|
|
317
324
|
var oapi = (0, index_1.default)({
|
|
318
325
|
info: {
|
|
319
326
|
description: "Generated docs from an Express api",
|
|
@@ -355,6 +362,9 @@ var TerrenoApp = /** @class */ (function () {
|
|
|
355
362
|
}
|
|
356
363
|
finally { if (e_2) throw e_2.error; }
|
|
357
364
|
}
|
|
365
|
+
if (options.configureApp) {
|
|
366
|
+
options.configureApp(app, { openApi: oapi });
|
|
367
|
+
}
|
|
358
368
|
// /auth/me must be registered after plugins so that session middleware
|
|
359
369
|
// (e.g. Better Auth) has a chance to populate req.user first.
|
|
360
370
|
(0, auth_1.addMeRoutes)(app, options.userModel, options.authOptions);
|