@terreno/api 0.20.2 → 0.22.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 (107) 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/bunfig.toml +1 -1
  5. package/dist/__tests__/versionCheckPlugin.test.js +29 -7
  6. package/dist/actions.openApi.test.js +13 -11
  7. package/dist/api.js +98 -11
  8. package/dist/api.query.test.js +31 -1
  9. package/dist/api.test.js +211 -0
  10. package/dist/auth.test.js +418 -43
  11. package/dist/betterAuth.d.ts +1 -1
  12. package/dist/consentApp.test.js +1 -0
  13. package/dist/example.js +4 -4
  14. package/dist/expressServer.d.ts +0 -22
  15. package/dist/expressServer.js +1 -125
  16. package/dist/expressServer.test.js +90 -91
  17. package/dist/githubAuth.test.js +22 -22
  18. package/dist/logger.d.ts +154 -0
  19. package/dist/logger.js +445 -26
  20. package/dist/logger.test.js +435 -0
  21. package/dist/middleware.d.ts +7 -0
  22. package/dist/middleware.js +58 -1
  23. package/dist/middleware.test.js +159 -0
  24. package/dist/models/consentForm.js +2 -1
  25. package/dist/models/consentResponse.js +2 -1
  26. package/dist/models/versionConfig.js +2 -1
  27. package/dist/openApi.test.js +10 -17
  28. package/dist/openApiBuilder.d.ts +18 -0
  29. package/dist/openApiBuilder.js +21 -0
  30. package/dist/openApiBuilder.test.js +34 -10
  31. package/dist/permissions.test.js +10 -43
  32. package/dist/populate.test.js +10 -42
  33. package/dist/realtime/changeStreamWatcher.d.ts +4 -4
  34. package/dist/realtime/changeStreamWatcher.js +2 -4
  35. package/dist/realtime/queryMatcher.d.ts +1 -1
  36. package/dist/realtime/queryMatcher.js +39 -14
  37. package/dist/realtime/types.d.ts +3 -3
  38. package/dist/requestContext.d.ts +61 -0
  39. package/dist/requestContext.js +74 -0
  40. package/dist/secretProviders.test.js +335 -0
  41. package/dist/syncConsents.test.js +2 -2
  42. package/dist/terrenoApp.d.ts +27 -15
  43. package/dist/terrenoApp.js +24 -14
  44. package/dist/terrenoApp.test.js +52 -0
  45. package/dist/tests/bunSetup.js +66 -262
  46. package/dist/tests/createTestData.d.ts +9 -0
  47. package/dist/tests/createTestData.js +272 -0
  48. package/dist/tests/models.d.ts +71 -0
  49. package/dist/tests/models.js +134 -0
  50. package/dist/tests/mongoTestSetup.d.ts +7 -0
  51. package/dist/tests/mongoTestSetup.js +150 -0
  52. package/dist/tests/testEnv.d.ts +0 -0
  53. package/dist/tests/testEnv.js +6 -0
  54. package/dist/tests/testHelper.d.ts +22 -0
  55. package/dist/tests/testHelper.js +115 -0
  56. package/dist/tests/types.d.ts +29 -0
  57. package/dist/tests/types.js +2 -0
  58. package/dist/tests.d.ts +10 -78
  59. package/dist/tests.js +24 -241
  60. package/dist/transformers.test.js +14 -50
  61. package/package.json +18 -4
  62. package/src/__snapshots__/openApiBuilder.test.ts.snap +1 -0
  63. package/src/__tests__/versionCheckPlugin.test.ts +43 -15
  64. package/src/actions.openApi.test.ts +12 -10
  65. package/src/api.query.test.ts +24 -1
  66. package/src/api.test.ts +169 -0
  67. package/src/api.ts +71 -0
  68. package/src/auth.test.ts +287 -39
  69. package/src/betterAuth.ts +1 -1
  70. package/src/consentApp.test.ts +1 -0
  71. package/src/example.ts +4 -4
  72. package/src/expressServer.test.ts +82 -85
  73. package/src/expressServer.ts +1 -213
  74. package/src/githubAuth.test.ts +22 -22
  75. package/src/logger.test.ts +466 -1
  76. package/src/logger.ts +477 -14
  77. package/src/middleware.test.ts +74 -2
  78. package/src/middleware.ts +57 -0
  79. package/src/models/consentForm.ts +3 -4
  80. package/src/models/consentResponse.ts +6 -4
  81. package/src/models/versionConfig.ts +3 -4
  82. package/src/openApi.test.ts +10 -17
  83. package/src/openApiBuilder.test.ts +27 -10
  84. package/src/openApiBuilder.ts +24 -0
  85. package/src/permissions.test.ts +8 -23
  86. package/src/populate.test.ts +7 -22
  87. package/src/realtime/changeStreamWatcher.ts +15 -10
  88. package/src/realtime/queryMatcher.ts +54 -27
  89. package/src/realtime/types.ts +4 -4
  90. package/src/requestContext.ts +86 -0
  91. package/src/secretProviders.test.ts +219 -1
  92. package/src/syncConsents.test.ts +1 -1
  93. package/src/terrenoApp.test.ts +38 -0
  94. package/src/terrenoApp.ts +37 -15
  95. package/src/tests/bunSetup.ts +22 -236
  96. package/src/tests/createTestData.ts +176 -0
  97. package/src/tests/models.ts +164 -0
  98. package/src/tests/mongoTestSetup.ts +69 -0
  99. package/src/tests/testEnv.ts +4 -0
  100. package/src/tests/testHelper.ts +57 -0
  101. package/src/tests/types.ts +35 -0
  102. package/src/tests.ts +40 -231
  103. package/src/transformers.test.ts +11 -30
  104. package/tsconfig.typedoc.json +4 -0
  105. package/dist/tests/index.d.ts +0 -1
  106. package/dist/tests/index.js +0 -17
  107. package/src/tests/index.ts +0 -1
@@ -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
+ });
@@ -50,7 +50,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
50
50
  var bun_test_1 = require("bun:test");
51
51
  var consentForm_1 = require("./models/consentForm");
52
52
  var syncConsents_1 = require("./syncConsents");
53
- var tests_1 = require("./tests");
53
+ var testHelper_1 = require("./tests/testHelper");
54
54
  var baseDef = {
55
55
  content: { en: "# Terms\nPlease agree." },
56
56
  order: 1,
@@ -62,7 +62,7 @@ var baseDef = {
62
62
  (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
63
63
  return __generator(this, function (_a) {
64
64
  switch (_a.label) {
65
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
65
+ case 0: return [4 /*yield*/, (0, testHelper_1.setupDb)()];
66
66
  case 1:
67
67
  _a.sent();
68
68
  return [4 /*yield*/, consentForm_1.ConsentForm.deleteMany({})];
@@ -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 provides an alternative to `setupServer` using a registration
46
- * pattern instead of callbacks. Build applications by registering model
47
- * routers and plugins, then calling `start()` to begin listening.
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. Custom middleware (via addMiddleware)
52
- * 3. JSON body parser
53
- * 4. Auth routes (/auth/login, /auth/signup, etc.)
54
- * 5. JWT authentication setup
55
- * 6. Request logging
56
- * 7. Sentry scopes
57
- * 8. OpenAPI middleware
58
- * 9. /auth/me routes
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. Registered model routers and plugins
61
- * 12. Error handling middleware
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
  */
@@ -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 provides an alternative to `setupServer` using a registration
82
- * pattern instead of callbacks. Build applications by registering model
83
- * routers and plugins, then calling `start()` to begin listening.
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. Custom middleware (via addMiddleware)
88
- * 3. JSON body parser
89
- * 4. Auth routes (/auth/login, /auth/signup, etc.)
90
- * 5. JWT authentication setup
91
- * 6. Request logging
92
- * 7. Sentry scopes
93
- * 8. OpenAPI middleware
94
- * 9. /auth/me routes
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. Registered model routers and plugins
97
- * 12. Error handling middleware
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);
@@ -104,6 +104,7 @@ var mongoose_1 = __importStar(require("mongoose"));
104
104
  var supertest_1 = __importDefault(require("supertest"));
105
105
  var api_1 = require("./api");
106
106
  var configurationPlugin_1 = require("./configurationPlugin");
107
+ var errors_1 = require("./errors");
107
108
  var permissions_1 = require("./permissions");
108
109
  var plugins_1 = require("./plugins");
109
110
  var terrenoApp_1 = require("./terrenoApp");
@@ -125,6 +126,24 @@ var typedUserModel = tests_1.UserModel;
125
126
  }).build();
126
127
  (0, bun_test_1.expect)(app).toBeDefined();
127
128
  });
129
+ (0, bun_test_1.it)("does not add requestId to GET /openapi.json document bodies", function () { return __awaiter(void 0, void 0, void 0, function () {
130
+ var app, res;
131
+ return __generator(this, function (_d) {
132
+ switch (_d.label) {
133
+ case 0:
134
+ app = new terrenoApp_1.TerrenoApp({
135
+ skipListen: true,
136
+ userModel: typedUserModel,
137
+ }).build();
138
+ return [4 /*yield*/, (0, supertest_1.default)(app).get("/openapi.json").expect(200)];
139
+ case 1:
140
+ res = _d.sent();
141
+ (0, bun_test_1.expect)(res.body.openapi).toBe("3.0.0");
142
+ (0, bun_test_1.expect)(res.body.requestId).toBeUndefined();
143
+ return [2 /*return*/];
144
+ }
145
+ });
146
+ }); });
128
147
  (0, bun_test_1.it)("creates server with custom corsOrigin", function () {
129
148
  var app = new terrenoApp_1.TerrenoApp({
130
149
  corsOrigin: "https://example.com",
@@ -196,6 +215,7 @@ var typedUserModel = tests_1.UserModel;
196
215
  res = _d.sent();
197
216
  (0, bun_test_1.expect)(res.body.data).toHaveLength(1);
198
217
  (0, bun_test_1.expect)(res.body.data[0].name).toBe("Apple");
218
+ (0, bun_test_1.expect)(res.body.requestId).toBe(res.headers["x-request-id"]);
199
219
  return [2 /*return*/];
200
220
  }
201
221
  });
@@ -301,6 +321,7 @@ var typedUserModel = tests_1.UserModel;
301
321
  case 2:
302
322
  res = _d.sent();
303
323
  (0, bun_test_1.expect)(res.status).toBe(200);
324
+ (0, bun_test_1.expect)(res.body.requestId).toBe(res.headers["x-request-id"]);
304
325
  return [2 /*return*/];
305
326
  }
306
327
  });
@@ -356,6 +377,37 @@ var typedUserModel = tests_1.UserModel;
356
377
  case 1:
357
378
  res = _d.sent();
358
379
  (0, bun_test_1.expect)(res.status).toBe(500);
380
+ (0, bun_test_1.expect)(res.body.requestId).toBe(res.headers["x-request-id"]);
381
+ (0, bun_test_1.expect)(res.body.status).toBe(500);
382
+ (0, bun_test_1.expect)(res.body.title).toBe("Internal server error");
383
+ return [2 /*return*/];
384
+ }
385
+ });
386
+ }); });
387
+ (0, bun_test_1.it)("adds requestId to APIError JSON responses", function () { return __awaiter(void 0, void 0, void 0, function () {
388
+ var plugin, app, res;
389
+ return __generator(this, function (_d) {
390
+ switch (_d.label) {
391
+ case 0:
392
+ plugin = {
393
+ register: function (pluginApp) {
394
+ pluginApp.get("/api-error-route", function () {
395
+ throw new errors_1.APIError({ status: 400, title: "Bad request test" });
396
+ });
397
+ },
398
+ };
399
+ app = new terrenoApp_1.TerrenoApp({
400
+ skipListen: true,
401
+ userModel: typedUserModel,
402
+ })
403
+ .register(plugin)
404
+ .build();
405
+ return [4 /*yield*/, (0, supertest_1.default)(app).get("/api-error-route").set("X-Request-ID", "api-err-rid")];
406
+ case 1:
407
+ res = _d.sent();
408
+ (0, bun_test_1.expect)(res.status).toBe(400);
409
+ (0, bun_test_1.expect)(res.body.requestId).toBe("api-err-rid");
410
+ (0, bun_test_1.expect)(res.body.title).toBe("Bad request test");
359
411
  return [2 /*return*/];
360
412
  }
361
413
  });