@tumbaland/backend-core 1.22.0 → 1.24.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 (73) hide show
  1. package/dist/entitlements/UsageMeter.d.ts +30 -0
  2. package/dist/entitlements/UsageMeter.d.ts.map +1 -0
  3. package/dist/entitlements/UsageMeter.js +27 -0
  4. package/dist/entitlements/UsageMeter.js.map +1 -0
  5. package/dist/entitlements/client.d.ts +17 -0
  6. package/dist/entitlements/client.d.ts.map +1 -0
  7. package/dist/entitlements/client.js +142 -0
  8. package/dist/entitlements/client.js.map +1 -0
  9. package/dist/entitlements/definitions.d.ts +124 -0
  10. package/dist/entitlements/definitions.d.ts.map +1 -0
  11. package/dist/entitlements/definitions.js +210 -0
  12. package/dist/entitlements/definitions.js.map +1 -0
  13. package/dist/entitlements/index.d.ts +12 -0
  14. package/dist/entitlements/index.d.ts.map +1 -0
  15. package/dist/entitlements/index.js +43 -0
  16. package/dist/entitlements/index.js.map +1 -0
  17. package/dist/entitlements/middleware.d.ts +54 -0
  18. package/dist/entitlements/middleware.d.ts.map +1 -0
  19. package/dist/entitlements/middleware.js +109 -0
  20. package/dist/entitlements/middleware.js.map +1 -0
  21. package/dist/entitlements/mode.d.ts +10 -0
  22. package/dist/entitlements/mode.d.ts.map +1 -0
  23. package/dist/entitlements/mode.js +32 -0
  24. package/dist/entitlements/mode.js.map +1 -0
  25. package/dist/entitlements/types.d.ts +29 -0
  26. package/dist/entitlements/types.d.ts.map +1 -0
  27. package/dist/entitlements/types.js +3 -0
  28. package/dist/entitlements/types.js.map +1 -0
  29. package/dist/entitlements/usage.d.ts +56 -0
  30. package/dist/entitlements/usage.d.ts.map +1 -0
  31. package/dist/entitlements/usage.js +159 -0
  32. package/dist/entitlements/usage.js.map +1 -0
  33. package/dist/errors/HttpError.d.ts +30 -1
  34. package/dist/errors/HttpError.d.ts.map +1 -1
  35. package/dist/errors/HttpError.js +20 -2
  36. package/dist/errors/HttpError.js.map +1 -1
  37. package/dist/index.d.ts +4 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +26 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/middleware/corsMiddleware.d.ts.map +1 -1
  42. package/dist/middleware/corsMiddleware.js +5 -1
  43. package/dist/middleware/corsMiddleware.js.map +1 -1
  44. package/dist/middleware/errorHandler.d.ts.map +1 -1
  45. package/dist/middleware/errorHandler.js +4 -1
  46. package/dist/middleware/errorHandler.js.map +1 -1
  47. package/dist/middleware/internalServiceAuth.d.ts +44 -0
  48. package/dist/middleware/internalServiceAuth.d.ts.map +1 -0
  49. package/dist/middleware/internalServiceAuth.js +86 -0
  50. package/dist/middleware/internalServiceAuth.js.map +1 -0
  51. package/package.json +2 -1
  52. package/src/entitlements/UsageMeter.ts +49 -0
  53. package/src/entitlements/client.test.ts +170 -0
  54. package/src/entitlements/client.ts +162 -0
  55. package/src/entitlements/definitions.test.ts +161 -0
  56. package/src/entitlements/definitions.ts +261 -0
  57. package/src/entitlements/index.ts +40 -0
  58. package/src/entitlements/middleware.test.ts +179 -0
  59. package/src/entitlements/middleware.ts +136 -0
  60. package/src/entitlements/mode.test.ts +50 -0
  61. package/src/entitlements/mode.ts +28 -0
  62. package/src/entitlements/types.ts +30 -0
  63. package/src/entitlements/usage.test.ts +271 -0
  64. package/src/entitlements/usage.ts +189 -0
  65. package/src/errors/HttpError.test.ts +39 -1
  66. package/src/errors/HttpError.ts +35 -1
  67. package/src/index.ts +13 -1
  68. package/src/middleware/corsMiddleware.test.ts +38 -0
  69. package/src/middleware/corsMiddleware.ts +5 -1
  70. package/src/middleware/errorHandler.test.ts +43 -1
  71. package/src/middleware/errorHandler.ts +6 -1
  72. package/src/middleware/internalServiceAuth.test.ts +173 -0
  73. package/src/middleware/internalServiceAuth.ts +96 -0
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = void 0;
3
+ exports.PaymentRequiredError = exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = void 0;
4
4
  /**
5
5
  * Base class for errors that should be turned into a specific HTTP response
6
6
  * by the shared errorHandler, instead of every controller hand-rolling
@@ -12,10 +12,17 @@ exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = e
12
12
  */
13
13
  class HttpError extends Error {
14
14
  statusCode;
15
- constructor(statusCode, message) {
15
+ /**
16
+ * Extra fields merged into the JSON body by the shared errorHandler. Used by
17
+ * errors the client has to *act* on rather than just display — a 402 tells
18
+ * the upgrade dialog which meter blocked and which plan lifts it.
19
+ */
20
+ details;
21
+ constructor(statusCode, message, details) {
16
22
  super(message);
17
23
  this.name = new.target.name;
18
24
  this.statusCode = statusCode;
25
+ this.details = details;
19
26
  // Restore the prototype chain (needed when compiling to ES2018 targets
20
27
  // where `extends Error` doesn't preserve instanceof checks correctly).
21
28
  Object.setPrototypeOf(this, new.target.prototype);
@@ -58,4 +65,15 @@ class TooManyRequestsError extends HttpError {
58
65
  }
59
66
  }
60
67
  exports.TooManyRequestsError = TooManyRequestsError;
68
+ /**
69
+ * 402, never 403. 403 means "you may not"; 402 means "you may, for money" —
70
+ * and the frontend needs to tell those apart to choose between an error toast
71
+ * and an upgrade dialog.
72
+ */
73
+ class PaymentRequiredError extends HttpError {
74
+ constructor(message, details) {
75
+ super(402, message, { ...details });
76
+ }
77
+ }
78
+ exports.PaymentRequiredError = PaymentRequiredError;
61
79
  //# sourceMappingURL=HttpError.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"HttpError.js","sourceRoot":"","sources":["../../src/errors/HttpError.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,MAAa,SAAU,SAAQ,KAAK;IAClC,UAAU,CAAS;IAEnB,YAAY,UAAkB,EAAE,OAAe;QAC7C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,uEAAuE;QACvE,uEAAuE;QACvE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAXD,8BAWC;AAED,MAAa,eAAgB,SAAQ,SAAS;IAC5C,YAAY,OAAO,GAAG,aAAa;QACjC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,0CAIC;AAED,MAAa,iBAAkB,SAAQ,SAAS;IAC9C,YAAY,OAAO,GAAG,cAAc;QAClC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,8CAIC;AAED,MAAa,cAAe,SAAQ,SAAS;IAC3C,YAAY,OAAO,GAAG,eAAe;QACnC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,wCAIC;AAED,MAAa,aAAc,SAAQ,SAAS;IAC1C,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,aAAc,SAAQ,SAAS;IAC1C,YAAY,OAAO,GAAG,UAAU;QAC9B,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,oBAAqB,SAAQ,SAAS;IACjD,YAAY,OAAO,GAAG,mBAAmB;QACvC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,oDAIC"}
1
+ {"version":3,"file":"HttpError.js","sourceRoot":"","sources":["../../src/errors/HttpError.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,MAAa,SAAU,SAAQ,KAAK;IAClC,UAAU,CAAS;IACnB;;;;OAIG;IACH,OAAO,CAA2B;IAElC,YAAY,UAAkB,EAAE,OAAe,EAAE,OAAiC;QAChF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,uEAAuE;QACvE,uEAAuE;QACvE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAlBD,8BAkBC;AAED,MAAa,eAAgB,SAAQ,SAAS;IAC5C,YAAY,OAAO,GAAG,aAAa;QACjC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,0CAIC;AAED,MAAa,iBAAkB,SAAQ,SAAS;IAC9C,YAAY,OAAO,GAAG,cAAc;QAClC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,8CAIC;AAED,MAAa,cAAe,SAAQ,SAAS;IAC3C,YAAY,OAAO,GAAG,eAAe;QACnC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,wCAIC;AAED,MAAa,aAAc,SAAQ,SAAS;IAC1C,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,aAAc,SAAQ,SAAS;IAC1C,YAAY,OAAO,GAAG,UAAU;QAC9B,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,oBAAqB,SAAQ,SAAS;IACjD,YAAY,OAAO,GAAG,mBAAmB;QACvC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,oDAIC;AAkBD;;;;GAIG;AACH,MAAa,oBAAqB,SAAQ,SAAS;IACjD,YAAY,OAAe,EAAE,OAA+B;QAC1D,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IACtC,CAAC;CACF;AAJD,oDAIC"}
package/dist/index.d.ts CHANGED
@@ -8,8 +8,11 @@ export { createHealthCheck } from './health/createHealthCheck';
8
8
  export type { HealthCheckConfig } from './health/createHealthCheck';
9
9
  export { connectDB, disconnectDB } from './database/connection';
10
10
  export { requireEnv } from './config/env';
11
- export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, TooManyRequestsError } from './errors/HttpError';
11
+ export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, TooManyRequestsError, PaymentRequiredError } from './errors/HttpError';
12
+ export type { PaymentRequiredDetails } from './errors/HttpError';
13
+ export * from './entitlements';
12
14
  export { authenticateToken } from './middleware/authMiddleware';
15
+ export { requireInternalServiceToken, internalServiceTokenBypass, allowUserOrInternalService, verifyInternalToken, INTERNAL_TOKEN_HEADER, SERVICE_ID_HEADER } from './middleware/internalServiceAuth';
13
16
  export { createCorsMiddleware } from './middleware/corsMiddleware';
14
17
  export { errorHandler } from './middleware/errorHandler';
15
18
  export { requestLogger, requestLoggerWithMetrics, simpleRequestLogger } from './middleware/requestLogger';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAGvJ,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC1G,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACnH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,UAAU,EACV,aAAa,EACb,kBAAkB,EACnB,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC7K,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAGjE,cAAc,gBAAgB,CAAC;AAG/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC1G,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACnH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,UAAU,EACV,aAAa,EACb,kBAAkB,EACnB,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -1,11 +1,25 @@
1
1
  "use strict";
2
2
  // @tumbaland/backend-core - Core shared functionality for Tumbaland backend services
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
16
+ };
3
17
  var __importDefault = (this && this.__importDefault) || function (mod) {
4
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
19
  };
6
20
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.injectHeaders = exports.setSpanTag = exports.logToSpan = exports.createChildSpan = exports.tracingMiddleware = exports.startSpan = exports.getTracer = exports.initTracer = exports.register = exports.businessMetrics = exports.databaseQueriesTotal = exports.databaseQueryDuration = exports.httpRequestsTotal = exports.httpRequestDuration = exports.metricsMiddleware = exports.canAccessResource = exports.buildAccessQuery = exports.sendError = exports.sendSuccess = exports.createApiResponse = exports.correlationMiddleware = exports.generateCorrelationId = exports.validate = exports.strictRateLimiter = exports.standardRateLimiter = exports.createRateLimiter = exports.securityHeaders = exports.simpleRequestLogger = exports.requestLoggerWithMetrics = exports.requestLogger = exports.errorHandler = exports.createCorsMiddleware = exports.authenticateToken = exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = exports.requireEnv = exports.disconnectDB = exports.connectDB = exports.createHealthCheck = exports.metricsHandler = exports.healthLive = exports.healthCheck = exports.logger = exports.registerShutdown = exports.createBaseApp = void 0;
8
- exports.extractSpanContext = void 0;
21
+ exports.initTracer = exports.register = exports.businessMetrics = exports.databaseQueriesTotal = exports.databaseQueryDuration = exports.httpRequestsTotal = exports.httpRequestDuration = exports.metricsMiddleware = exports.canAccessResource = exports.buildAccessQuery = exports.sendError = exports.sendSuccess = exports.createApiResponse = exports.correlationMiddleware = exports.generateCorrelationId = exports.validate = exports.strictRateLimiter = exports.standardRateLimiter = exports.createRateLimiter = exports.securityHeaders = exports.simpleRequestLogger = exports.requestLoggerWithMetrics = exports.requestLogger = exports.errorHandler = exports.createCorsMiddleware = exports.SERVICE_ID_HEADER = exports.INTERNAL_TOKEN_HEADER = exports.verifyInternalToken = exports.allowUserOrInternalService = exports.internalServiceTokenBypass = exports.requireInternalServiceToken = exports.authenticateToken = exports.PaymentRequiredError = exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = exports.requireEnv = exports.disconnectDB = exports.connectDB = exports.createHealthCheck = exports.metricsHandler = exports.healthLive = exports.healthCheck = exports.logger = exports.registerShutdown = exports.createBaseApp = void 0;
22
+ exports.extractSpanContext = exports.injectHeaders = exports.setSpanTag = exports.logToSpan = exports.createChildSpan = exports.tracingMiddleware = exports.startSpan = exports.getTracer = void 0;
9
23
  // App bootstrap
10
24
  var createBaseApp_1 = require("./app/createBaseApp");
11
25
  Object.defineProperty(exports, "createBaseApp", { enumerable: true, get: function () { return createBaseApp_1.createBaseApp; } });
@@ -37,9 +51,19 @@ Object.defineProperty(exports, "ForbiddenError", { enumerable: true, get: functi
37
51
  Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return HttpError_1.NotFoundError; } });
38
52
  Object.defineProperty(exports, "ConflictError", { enumerable: true, get: function () { return HttpError_1.ConflictError; } });
39
53
  Object.defineProperty(exports, "TooManyRequestsError", { enumerable: true, get: function () { return HttpError_1.TooManyRequestsError; } });
54
+ Object.defineProperty(exports, "PaymentRequiredError", { enumerable: true, get: function () { return HttpError_1.PaymentRequiredError; } });
55
+ // Entitlements (plan limits, usage meters, enforcement)
56
+ __exportStar(require("./entitlements"), exports);
40
57
  // Middleware
41
58
  var authMiddleware_1 = require("./middleware/authMiddleware");
42
59
  Object.defineProperty(exports, "authenticateToken", { enumerable: true, get: function () { return authMiddleware_1.authenticateToken; } });
60
+ var internalServiceAuth_1 = require("./middleware/internalServiceAuth");
61
+ Object.defineProperty(exports, "requireInternalServiceToken", { enumerable: true, get: function () { return internalServiceAuth_1.requireInternalServiceToken; } });
62
+ Object.defineProperty(exports, "internalServiceTokenBypass", { enumerable: true, get: function () { return internalServiceAuth_1.internalServiceTokenBypass; } });
63
+ Object.defineProperty(exports, "allowUserOrInternalService", { enumerable: true, get: function () { return internalServiceAuth_1.allowUserOrInternalService; } });
64
+ Object.defineProperty(exports, "verifyInternalToken", { enumerable: true, get: function () { return internalServiceAuth_1.verifyInternalToken; } });
65
+ Object.defineProperty(exports, "INTERNAL_TOKEN_HEADER", { enumerable: true, get: function () { return internalServiceAuth_1.INTERNAL_TOKEN_HEADER; } });
66
+ Object.defineProperty(exports, "SERVICE_ID_HEADER", { enumerable: true, get: function () { return internalServiceAuth_1.SERVICE_ID_HEADER; } });
43
67
  var corsMiddleware_1 = require("./middleware/corsMiddleware");
44
68
  Object.defineProperty(exports, "createCorsMiddleware", { enumerable: true, get: function () { return corsMiddleware_1.createCorsMiddleware; } });
45
69
  var errorHandler_1 = require("./middleware/errorHandler");
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,qFAAqF;;;;;;;AAErF,gBAAgB;AAChB,qDAAoD;AAA3C,8GAAA,aAAa,OAAA;AAEtB,2CAAkD;AAAzC,4GAAA,gBAAgB,OAAA;AAGzB,UAAU;AACV,2CAAqD;AAA5C,iHAAA,OAAO,OAAU;AAE1B,gBAAgB;AAChB,8DAAoF;AAA3E,+GAAA,WAAW,OAAA;AAAE,8GAAA,UAAU,OAAA;AAAE,kHAAA,cAAc,OAAA;AAChD,gEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAG1B,WAAW;AACX,oDAAgE;AAAvD,uGAAA,SAAS,OAAA;AAAE,0GAAA,YAAY,OAAA;AAEhC,SAAS;AACT,oCAA0C;AAAjC,iGAAA,UAAU,OAAA;AAEnB,SAAS;AACT,gDAAuJ;AAA9I,sGAAA,SAAS,OAAA;AAAE,4GAAA,eAAe,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAAE,2GAAA,cAAc,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAE1H,aAAa;AACb,8DAAgE;AAAvD,mHAAA,iBAAiB,OAAA;AAC1B,8DAAmE;AAA1D,sHAAA,oBAAoB,OAAA;AAC7B,0DAAyD;AAAhD,4GAAA,YAAY,OAAA;AACrB,4DAA0G;AAAjG,8GAAA,aAAa,OAAA;AAAE,yHAAA,wBAAwB,OAAA;AAAE,oHAAA,mBAAmB,OAAA;AACrE,kDAAmH;AAA1G,2GAAA,eAAe,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AAAE,+GAAA,mBAAmB,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AACnF,kDAAiD;AAAxC,oGAAA,QAAQ,OAAA;AAKjB,QAAQ;AACR,mDAAmF;AAA1E,oHAAA,qBAAqB,OAAA;AAAE,oHAAA,qBAAqB,OAAA;AACrD,6CAA6E;AAApE,6GAAA,iBAAiB,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,qGAAA,SAAS,OAAA;AAElD,2DAA8E;AAArE,mHAAA,gBAAgB,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAE5C,UAAU;AACV,qCAQmB;AAPjB,4GAAA,iBAAiB,OAAA;AACjB,8GAAA,mBAAmB,OAAA;AACnB,4GAAA,iBAAiB,OAAA;AACjB,gHAAA,qBAAqB,OAAA;AACrB,+GAAA,oBAAoB,OAAA;AACpB,0GAAA,eAAe,OAAA;AACf,mGAAA,QAAQ,OAAA;AAGV,UAAU;AACV,qCAUmB;AATjB,qGAAA,UAAU,OAAA;AACV,oGAAA,SAAS,OAAA;AACT,oGAAA,SAAS,OAAA;AACT,4GAAA,iBAAiB,OAAA;AACjB,0GAAA,eAAe,OAAA;AACf,oGAAA,SAAS,OAAA;AACT,qGAAA,UAAU,OAAA;AACV,wGAAA,aAAa,OAAA;AACb,6GAAA,kBAAkB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,qFAAqF;;;;;;;;;;;;;;;;;;;;;AAErF,gBAAgB;AAChB,qDAAoD;AAA3C,8GAAA,aAAa,OAAA;AAEtB,2CAAkD;AAAzC,4GAAA,gBAAgB,OAAA;AAGzB,UAAU;AACV,2CAAqD;AAA5C,iHAAA,OAAO,OAAU;AAE1B,gBAAgB;AAChB,8DAAoF;AAA3E,+GAAA,WAAW,OAAA;AAAE,8GAAA,UAAU,OAAA;AAAE,kHAAA,cAAc,OAAA;AAChD,gEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAG1B,WAAW;AACX,oDAAgE;AAAvD,uGAAA,SAAS,OAAA;AAAE,0GAAA,YAAY,OAAA;AAEhC,SAAS;AACT,oCAA0C;AAAjC,iGAAA,UAAU,OAAA;AAEnB,SAAS;AACT,gDAA6K;AAApK,sGAAA,SAAS,OAAA;AAAE,4GAAA,eAAe,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAAE,2GAAA,cAAc,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAGhJ,wDAAwD;AACxD,iDAA+B;AAE/B,aAAa;AACb,8DAAgE;AAAvD,mHAAA,iBAAiB,OAAA;AAC1B,wEAO0C;AANxC,kIAAA,2BAA2B,OAAA;AAC3B,iIAAA,0BAA0B,OAAA;AAC1B,iIAAA,0BAA0B,OAAA;AAC1B,0HAAA,mBAAmB,OAAA;AACnB,4HAAA,qBAAqB,OAAA;AACrB,wHAAA,iBAAiB,OAAA;AAEnB,8DAAmE;AAA1D,sHAAA,oBAAoB,OAAA;AAC7B,0DAAyD;AAAhD,4GAAA,YAAY,OAAA;AACrB,4DAA0G;AAAjG,8GAAA,aAAa,OAAA;AAAE,yHAAA,wBAAwB,OAAA;AAAE,oHAAA,mBAAmB,OAAA;AACrE,kDAAmH;AAA1G,2GAAA,eAAe,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AAAE,+GAAA,mBAAmB,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AACnF,kDAAiD;AAAxC,oGAAA,QAAQ,OAAA;AAKjB,QAAQ;AACR,mDAAmF;AAA1E,oHAAA,qBAAqB,OAAA;AAAE,oHAAA,qBAAqB,OAAA;AACrD,6CAA6E;AAApE,6GAAA,iBAAiB,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,qGAAA,SAAS,OAAA;AAElD,2DAA8E;AAArE,mHAAA,gBAAgB,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAE5C,UAAU;AACV,qCAQmB;AAPjB,4GAAA,iBAAiB,OAAA;AACjB,8GAAA,mBAAmB,OAAA;AACnB,4GAAA,iBAAiB,OAAA;AACjB,gHAAA,qBAAqB,OAAA;AACrB,+GAAA,oBAAoB,OAAA;AACpB,0GAAA,eAAe,OAAA;AACf,mGAAA,QAAQ,OAAA;AAGV,UAAU;AACV,qCAUmB;AATjB,qGAAA,UAAU,OAAA;AACV,oGAAA,SAAS,OAAA;AACT,oGAAA,SAAS,OAAA;AACT,4GAAA,iBAAiB,OAAA;AACjB,0GAAA,eAAe,OAAA;AACf,oGAAA,SAAS,OAAA;AACT,qGAAA,UAAU,OAAA;AACV,wGAAA,aAAa,OAAA;AACb,6GAAA,kBAAkB,OAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"corsMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/corsMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B;cAmBiE,CAAC;;;aAAmH,CAAC,uBAD7P"}
1
+ {"version":3,"file":"corsMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/corsMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B;cAmBlE,CAAC;;;aACI,CAAC,uBAEX"}
@@ -25,7 +25,11 @@ function createCorsMiddleware(options = {}) {
25
25
  return callback(new Error('Not allowed by CORS: ' + origin));
26
26
  },
27
27
  credentials: true,
28
- methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
28
+ // PATCH belongs here as much as PUT: leaving it out lets the preflight
29
+ // succeed and then has the browser block the request it just approved,
30
+ // which surfaces in the UI as a bare "failed to save" with a 204 in the
31
+ // network tab and no failing request to point at.
32
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
29
33
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-correlation-id', 'x-session-id'],
30
34
  exposedHeaders: options.exposedHeaders
31
35
  });
@@ -1 +1 @@
1
- {"version":3,"file":"corsMiddleware.js","sourceRoot":"","sources":["../../src/middleware/corsMiddleware.ts"],"names":[],"mappings":";;;;;AAiBA,oDAkBC;AAnCD,gDAAwB;AACxB,+DAAuC;AAWvC;;;;GAIG;AACH,SAAgB,oBAAoB,CAAC,UAAiC,EAAE;IACtE,OAAO,IAAA,cAAI,EAAC;QACV,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;YAC3B,MAAM,cAAc,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC;iBACtE,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBAClB,MAAM,CAAC,OAAO,CAAC,CAAC;YACnB,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC/C,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC9B,CAAC;YACD,gBAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/C,OAAO,QAAQ,CAAC,IAAI,KAAK,CAAC,uBAAuB,GAAG,MAAM,CAAC,CAAC,CAAC;QAC/D,CAAC;QACD,WAAW,EAAE,IAAI;QACjB,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC;QACpD,cAAc,EAAE,CAAC,cAAc,EAAE,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,cAAc,CAAC;QACzG,cAAc,EAAE,OAAO,CAAC,cAAc;KACvC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"corsMiddleware.js","sourceRoot":"","sources":["../../src/middleware/corsMiddleware.ts"],"names":[],"mappings":";;;;;AAiBA,oDAsBC;AAvCD,gDAAwB;AACxB,+DAAuC;AAWvC;;;;GAIG;AACH,SAAgB,oBAAoB,CAAC,UAAiC,EAAE;IACtE,OAAO,IAAA,cAAI,EAAC;QACV,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;YAC3B,MAAM,cAAc,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC;iBACtE,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBAClB,MAAM,CAAC,OAAO,CAAC,CAAC;YACnB,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC/C,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC9B,CAAC;YACD,gBAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/C,OAAO,QAAQ,CAAC,IAAI,KAAK,CAAC,uBAAuB,GAAG,MAAM,CAAC,CAAC,CAAC;QAC/D,CAAC;QACD,WAAW,EAAE,IAAI;QACjB,uEAAuE;QACvE,uEAAuE;QACvE,wEAAwE;QACxE,kDAAkD;QAClD,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC;QAC7D,cAAc,EAAE,CAAC,cAAc,EAAE,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,cAAc,CAAC;QACzG,cAAc,EAAE,OAAO,CAAC,cAAc;KACvC,CAAC,CAAC;AACL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA4B/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,EAAE,mBA0D1B,CAAC"}
1
+ {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA8B/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,EAAE,mBA6D1B,CAAC"}
@@ -16,7 +16,7 @@ const HttpError_1 = require("../errors/HttpError");
16
16
  */
17
17
  function classifyError(error) {
18
18
  if (error instanceof HttpError_1.HttpError) {
19
- return { statusCode: error.statusCode, name: error.name, message: error.message };
19
+ return { statusCode: error.statusCode, name: error.name, message: error.message, details: error.details };
20
20
  }
21
21
  if (error.name === 'ValidationError' || error.name === 'CastError') {
22
22
  return { statusCode: 400, name: error.name, message: error.message };
@@ -68,6 +68,9 @@ const errorHandler = (error, req, res, _next) => {
68
68
  }
69
69
  if (known) {
70
70
  res.status(known.statusCode).json({
71
+ // Spread first so a stray details key can't shadow the envelope fields
72
+ // every client already relies on.
73
+ ...known.details,
71
74
  success: false,
72
75
  error: known.name,
73
76
  message: known.message
@@ -1 +1 @@
1
- {"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":";;;;;;AACA,+DAAuC;AACvC,mDAAgD;AAUhD;;;;;;;GAOG;AACH,SAAS,aAAa,CAAC,KAAY;IACjC,IAAI,KAAK,YAAY,qBAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IAC5G,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACnE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IACvE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACI,MAAM,YAAY,GAAwB,CAC/C,KAAY,EACZ,GAAY,EACZ,GAAa,EACb,KAAmB,EACb,EAAE;IACR,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAEnC,IAAI,KAAK,EAAE,CAAC;QACV,uEAAuE;QACvE,kEAAkE;QAClE,gBAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE;YAC7B,KAAK,EAAE,KAAK,CAAC,OAAO;YACpB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,aAAa,EAAE,GAAG,CAAC,aAAa;SACjC,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,gBAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE;YAC/B,KAAK,EAAE,KAAK,CAAC,OAAO;YACpB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;YAChC,aAAa,EAAE,GAAG,CAAC,aAAa;SACjC,CAAC,CAAC;IACL,CAAC;IAED,sCAAsC;IACtC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC;IAClC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC;QAChF,GAAG,CAAC,MAAM,CAAC,6BAA6B,EAAE,MAAM,CAAC,CAAC;QAClD,GAAG,CAAC,MAAM,CAAC,kCAAkC,EAAE,MAAM,CAAC,CAAC;QACvD,GAAG,CAAC,MAAM,CAAC,8BAA8B,EAAE,iCAAiC,CAAC,CAAC;QAC9E,GAAG,CAAC,MAAM,CAAC,8BAA8B,EAAE,+EAA+E,CAAC,CAAC;IAC9H,CAAC;IAED,IAAI,KAAK,EAAE,CAAC;QACV,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YAChC,uEAAuE;YACvE,kCAAkC;YAClC,GAAG,KAAK,CAAC,OAAO;YAChB,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,KAAK,CAAC,IAAI;YACjB,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,+DAA+D;IAC/D,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,CAAC;IAC7D,MAAM,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;IAE5E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,uBAAuB;QAC9B,OAAO,EAAE,YAAY;QACrB,GAAG,CAAC,aAAa,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;KAC7C,CAAC,CAAC;AACL,CAAC,CAAC;AA7DW,QAAA,YAAY,gBA6DvB"}
@@ -0,0 +1,44 @@
1
+ import { RequestHandler } from 'express';
2
+ /**
3
+ * Fields set by the internal-service middlewares below. Declared globally so
4
+ * routers and controllers read `req.isInternalService` off the same `Request`
5
+ * type Express itself uses, with no cast at the call site.
6
+ */
7
+ declare global {
8
+ namespace Express {
9
+ interface Request {
10
+ isInternalService?: boolean;
11
+ serviceIdentifier?: string;
12
+ }
13
+ }
14
+ }
15
+ export declare const INTERNAL_TOKEN_HEADER = "x-internal-token";
16
+ export declare const SERVICE_ID_HEADER = "x-service-id";
17
+ /**
18
+ * Constant-time comparison of the presented token against the configured one.
19
+ *
20
+ * `===` on secrets leaks their prefix through timing, and `timingSafeEqual`
21
+ * throws on a length mismatch (which is itself a leak), so length is compared
22
+ * first and both buffers are hashed to a fixed width — a token of the wrong
23
+ * length takes the same path as one of the right length with wrong bytes.
24
+ */
25
+ export declare function verifyInternalToken(presented: unknown): boolean;
26
+ /**
27
+ * Hard gate for endpoints that are only ever called service-to-service.
28
+ * No user-JWT fallback: a missing or wrong token is always a 401.
29
+ */
30
+ export declare const requireInternalServiceToken: RequestHandler;
31
+ /**
32
+ * For routers that serve both browsers and services: when the internal-token
33
+ * header is present it is *verified here*, and a bad token is rejected rather
34
+ * than passed along.
35
+ *
36
+ * The bug this replaces: treating the mere presence of the header as grounds to
37
+ * skip JWT verification, leaving the real check to a second middleware mounted
38
+ * per-router — so any route that forgot to mount the second one was reachable
39
+ * by sending `x-internal-token: anything`.
40
+ */
41
+ export declare const internalServiceTokenBypass: RequestHandler;
42
+ /** Allows a request that authenticated as either a user or an internal service. */
43
+ export declare const allowUserOrInternalService: RequestHandler;
44
+ //# sourceMappingURL=internalServiceAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internalServiceAuth.d.ts","sourceRoot":"","sources":["../../src/middleware/internalServiceAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,cAAc,EAAE,MAAM,SAAS,CAAC;AAIlD;;;;GAIG;AACH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,iBAAiB,CAAC,EAAE,OAAO,CAAC;YAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;SAC5B;KACF;CACF;AAED,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AACxD,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAS/D;AAOD;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,cAkBzC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,EAAE,cAOxC,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,EAAE,cAMxC,CAAC"}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.allowUserOrInternalService = exports.internalServiceTokenBypass = exports.requireInternalServiceToken = exports.SERVICE_ID_HEADER = exports.INTERNAL_TOKEN_HEADER = void 0;
7
+ exports.verifyInternalToken = verifyInternalToken;
8
+ const crypto_1 = require("crypto");
9
+ const logger_1 = __importDefault(require("../logging/logger"));
10
+ exports.INTERNAL_TOKEN_HEADER = 'x-internal-token';
11
+ exports.SERVICE_ID_HEADER = 'x-service-id';
12
+ /**
13
+ * Constant-time comparison of the presented token against the configured one.
14
+ *
15
+ * `===` on secrets leaks their prefix through timing, and `timingSafeEqual`
16
+ * throws on a length mismatch (which is itself a leak), so length is compared
17
+ * first and both buffers are hashed to a fixed width — a token of the wrong
18
+ * length takes the same path as one of the right length with wrong bytes.
19
+ */
20
+ function verifyInternalToken(presented) {
21
+ const expected = process.env.INTERNAL_SERVICE_TOKEN;
22
+ if (!expected)
23
+ return false;
24
+ if (typeof presented !== 'string' || presented.length === 0)
25
+ return false;
26
+ const a = Buffer.from(presented, 'utf8');
27
+ const b = Buffer.from(expected, 'utf8');
28
+ if (a.length !== b.length)
29
+ return false;
30
+ return (0, crypto_1.timingSafeEqual)(a, b);
31
+ }
32
+ function markInternal(req) {
33
+ req.isInternalService = true;
34
+ req.serviceIdentifier = req.headers[exports.SERVICE_ID_HEADER] || 'unknown';
35
+ }
36
+ /**
37
+ * Hard gate for endpoints that are only ever called service-to-service.
38
+ * No user-JWT fallback: a missing or wrong token is always a 401.
39
+ */
40
+ const requireInternalServiceToken = (req, res, next) => {
41
+ if (!process.env.INTERNAL_SERVICE_TOKEN) {
42
+ logger_1.default.error('INTERNAL_SERVICE_TOKEN not configured');
43
+ res.status(500).json({ success: false, message: 'Internal service authentication not configured' });
44
+ return;
45
+ }
46
+ if (!verifyInternalToken(req.headers[exports.INTERNAL_TOKEN_HEADER])) {
47
+ logger_1.default.warn('Rejected internal service request', {
48
+ serviceId: req.headers[exports.SERVICE_ID_HEADER] || 'unknown',
49
+ path: req.originalUrl
50
+ });
51
+ res.status(401).json({ success: false, message: 'Invalid or missing internal service token' });
52
+ return;
53
+ }
54
+ markInternal(req);
55
+ next();
56
+ };
57
+ exports.requireInternalServiceToken = requireInternalServiceToken;
58
+ /**
59
+ * For routers that serve both browsers and services: when the internal-token
60
+ * header is present it is *verified here*, and a bad token is rejected rather
61
+ * than passed along.
62
+ *
63
+ * The bug this replaces: treating the mere presence of the header as grounds to
64
+ * skip JWT verification, leaving the real check to a second middleware mounted
65
+ * per-router — so any route that forgot to mount the second one was reachable
66
+ * by sending `x-internal-token: anything`.
67
+ */
68
+ const internalServiceTokenBypass = (req, res, next) => {
69
+ const presented = req.headers[exports.INTERNAL_TOKEN_HEADER];
70
+ if (presented === undefined) {
71
+ next();
72
+ return;
73
+ }
74
+ (0, exports.requireInternalServiceToken)(req, res, next);
75
+ };
76
+ exports.internalServiceTokenBypass = internalServiceTokenBypass;
77
+ /** Allows a request that authenticated as either a user or an internal service. */
78
+ const allowUserOrInternalService = (req, res, next) => {
79
+ if (req.isInternalService || req.user) {
80
+ next();
81
+ return;
82
+ }
83
+ res.status(401).json({ success: false, message: 'Authentication required' });
84
+ };
85
+ exports.allowUserOrInternalService = allowUserOrInternalService;
86
+ //# sourceMappingURL=internalServiceAuth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internalServiceAuth.js","sourceRoot":"","sources":["../../src/middleware/internalServiceAuth.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tumbaland/backend-core",
3
- "version": "1.22.0",
3
+ "version": "1.24.0",
4
4
  "description": "Core shared functionality for Tumbaland backend services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,6 +36,7 @@
36
36
  "@types/morgan": "^1.9.10",
37
37
  "@types/node": "^26.1.0",
38
38
  "commit-and-tag-version": "^13.1.2",
39
+ "mongodb-memory-server": "^11.2.0",
39
40
  "typescript": "^6.0.3"
40
41
  },
41
42
  "dependencies": {
@@ -0,0 +1,49 @@
1
+ import { Schema, model, models, Document, Model } from 'mongoose';
2
+ import { MeterKey, MeterPeriod, meterPeriod } from './definitions';
3
+
4
+ /**
5
+ * One counter per (subject, meter, period). Lives in the shared database, so
6
+ * album-service can write `storageBytes` while payment-service reads it back
7
+ * for the pricing page without another round trip.
8
+ *
9
+ * `subjectId` is whoever the usage is billed to — an email for personal
10
+ * meters, the group owner's email for pooled ones. It is deliberately not a
11
+ * user ObjectId: every service already has the email on the JWT, and none of
12
+ * them share a users collection.
13
+ */
14
+ export interface IUsageMeter extends Document {
15
+ subjectId: string;
16
+ meter: MeterKey;
17
+ /** `ALL` for cumulative meters, `YYYY-MM` for monthly ones. */
18
+ period: string;
19
+ count: number;
20
+ createdAt: Date;
21
+ updatedAt: Date;
22
+ }
23
+
24
+ const UsageMeterSchema = new Schema<IUsageMeter>(
25
+ {
26
+ subjectId: { type: String, required: true, lowercase: true, trim: true },
27
+ meter: { type: String, required: true },
28
+ period: { type: String, required: true, default: 'ALL' },
29
+ count: { type: Number, required: true, default: 0 }
30
+ },
31
+ { timestamps: true }
32
+ );
33
+
34
+ UsageMeterSchema.index({ subjectId: 1, meter: 1, period: 1 }, { unique: true });
35
+
36
+ /**
37
+ * Registered through `models` first: several services import this module into
38
+ * the same mongoose singleton during tests, and a second `model()` call with
39
+ * the same name throws `OverwriteModelError`.
40
+ */
41
+ export const UsageMeter: Model<IUsageMeter> =
42
+ (models.UsageMeter as Model<IUsageMeter>) || model<IUsageMeter>('UsageMeter', UsageMeterSchema);
43
+
44
+ /** The period bucket a meter counts into right now. Monthly meters roll over at 00:00 UTC on the 1st. */
45
+ export function currentPeriod(meter: MeterKey, now: Date = new Date()): string {
46
+ const period: MeterPeriod = meterPeriod(meter);
47
+ if (period === 'ALL') return 'ALL';
48
+ return now.toISOString().slice(0, 7); // YYYY-MM
49
+ }
@@ -0,0 +1,170 @@
1
+ jest.mock('../logging/logger', () => ({
2
+ __esModule: true,
3
+ default: { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn(), http: jest.fn() }
4
+ }));
5
+
6
+ import logger from '../logging/logger';
7
+ import { clearEntitlementsCache, getEntitlements, invalidateEntitlements } from './client';
8
+ import { DEFAULT_PLAN_LIMITS } from './definitions';
9
+
10
+ const ORIGINAL_ENV = process.env;
11
+ const ORIGINAL_FETCH = global.fetch;
12
+
13
+ function jsonResponse(body: unknown, status = 200): Response {
14
+ return { ok: status >= 200 && status < 300, status, json: async () => body } as Response;
15
+ }
16
+
17
+ function paidResponse() {
18
+ return jsonResponse({
19
+ success: true,
20
+ planCode: 'personal',
21
+ status: 'active',
22
+ limits: { meters: { storageBytes: 12345 } },
23
+ upgradeTo: 'family'
24
+ });
25
+ }
26
+
27
+ beforeEach(() => {
28
+ process.env = {
29
+ ...ORIGINAL_ENV,
30
+ PAYMENT_API_URL: 'http://payment-service:5005',
31
+ INTERNAL_SERVICE_TOKEN: 'test-internal-token',
32
+ SERVICE_NAME: 'album-service'
33
+ };
34
+ clearEntitlementsCache();
35
+ global.fetch = jest.fn();
36
+ (logger.error as jest.Mock).mockClear();
37
+ });
38
+
39
+ afterAll(() => {
40
+ process.env = ORIGINAL_ENV;
41
+ global.fetch = ORIGINAL_FETCH;
42
+ });
43
+
44
+ describe('getEntitlements', () => {
45
+ it('calls the internal endpoint with the service token and normalizes the answer', async () => {
46
+ (global.fetch as jest.Mock).mockResolvedValue(paidResponse());
47
+
48
+ const result = await getEntitlements('User@Example.com');
49
+
50
+ expect(global.fetch).toHaveBeenCalledWith(
51
+ 'http://payment-service:5005/internal/entitlements?email=user%40example.com',
52
+ expect.objectContaining({
53
+ headers: expect.objectContaining({ 'x-internal-token': 'test-internal-token', 'x-service-id': 'album-service' })
54
+ })
55
+ );
56
+ expect(result.planCode).toBe('personal');
57
+ expect(result.upgradeTo).toBe('family');
58
+ expect(result.limits.meters.storageBytes).toBe(12345);
59
+ // Keys the payload omitted are filled from that plan's defaults rather than
60
+ // arriving undefined and comparing as NaN inside a quota check.
61
+ expect(result.limits.meters.seats).toBe(DEFAULT_PLAN_LIMITS.personal.meters.seats);
62
+ expect(result.stale).toBeUndefined();
63
+ });
64
+
65
+ it('serves the cached answer within the TTL and refetches after it', async () => {
66
+ jest.useFakeTimers();
67
+ try {
68
+ (global.fetch as jest.Mock).mockResolvedValue(paidResponse());
69
+
70
+ await getEntitlements('user@example.com');
71
+ await getEntitlements('user@example.com');
72
+ expect(global.fetch).toHaveBeenCalledTimes(1);
73
+
74
+ jest.advanceTimersByTime(60_001);
75
+ await getEntitlements('user@example.com');
76
+ expect(global.fetch).toHaveBeenCalledTimes(2);
77
+ } finally {
78
+ jest.useRealTimers();
79
+ }
80
+ });
81
+
82
+ it('treats casing and surrounding whitespace as the same user', async () => {
83
+ (global.fetch as jest.Mock).mockResolvedValue(paidResponse());
84
+
85
+ await getEntitlements('user@example.com');
86
+ await getEntitlements(' User@Example.COM ');
87
+
88
+ expect(global.fetch).toHaveBeenCalledTimes(1);
89
+ });
90
+
91
+ it('collapses a concurrent burst for one user into a single request', async () => {
92
+ (global.fetch as jest.Mock).mockResolvedValue(paidResponse());
93
+
94
+ await Promise.all(Array.from({ length: 10 }, () => getEntitlements('user@example.com')));
95
+
96
+ expect(global.fetch).toHaveBeenCalledTimes(1);
97
+ });
98
+
99
+ it('drops the cached answer on invalidate', async () => {
100
+ (global.fetch as jest.Mock).mockResolvedValue(paidResponse());
101
+
102
+ await getEntitlements('user@example.com');
103
+ invalidateEntitlements('User@example.com');
104
+ await getEntitlements('user@example.com');
105
+
106
+ expect(global.fetch).toHaveBeenCalledTimes(2);
107
+ });
108
+ });
109
+
110
+ describe('when payment-service cannot be reached', () => {
111
+ it('fails closed to free limits rather than handing out the product', async () => {
112
+ (global.fetch as jest.Mock).mockRejectedValue(new Error('ECONNREFUSED'));
113
+
114
+ const result = await getEntitlements('user@example.com');
115
+
116
+ expect(result.planCode).toBe('free');
117
+ expect(result.limits).toEqual(DEFAULT_PLAN_LIMITS.free);
118
+ expect(result.stale).toBe(true);
119
+ expect(logger.error).toHaveBeenCalled();
120
+ });
121
+
122
+ it('fails closed on a non-2xx answer too', async () => {
123
+ (global.fetch as jest.Mock).mockResolvedValue(jsonResponse({ success: false }, 500));
124
+
125
+ const result = await getEntitlements('user@example.com');
126
+
127
+ expect(result.planCode).toBe('free');
128
+ expect(result.stale).toBe(true);
129
+ });
130
+
131
+ it('never caches a fallback, so recovery is immediate rather than a minute later', async () => {
132
+ (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('ECONNREFUSED')).mockResolvedValue(paidResponse());
133
+
134
+ const failed = await getEntitlements('user@example.com');
135
+ const recovered = await getEntitlements('user@example.com');
136
+
137
+ expect(failed.stale).toBe(true);
138
+ expect(recovered.planCode).toBe('personal');
139
+ });
140
+
141
+ it('fails closed without calling out at all when the channel is unconfigured', async () => {
142
+ delete process.env.PAYMENT_API_URL;
143
+
144
+ const result = await getEntitlements('user@example.com');
145
+
146
+ expect(global.fetch).not.toHaveBeenCalled();
147
+ expect(result.planCode).toBe('free');
148
+ expect(result.stale).toBe(true);
149
+ expect(logger.error).toHaveBeenCalledWith('Entitlement lookup not configured', expect.anything());
150
+ });
151
+ });
152
+
153
+ describe('cache bounds', () => {
154
+ it('evicts the oldest entries instead of growing without limit', async () => {
155
+ (global.fetch as jest.Mock).mockResolvedValue(paidResponse());
156
+
157
+ for (let i = 0; i < 5_001; i++) {
158
+ await getEntitlements(`user${i}@example.com`);
159
+ }
160
+ const callsAfterFill = (global.fetch as jest.Mock).mock.calls.length;
161
+
162
+ // The very first user was evicted by the 5001st, so this is a refetch.
163
+ await getEntitlements('user0@example.com');
164
+ expect((global.fetch as jest.Mock).mock.calls.length).toBe(callsAfterFill + 1);
165
+
166
+ // The most recent user is still cached.
167
+ await getEntitlements('user5000@example.com');
168
+ expect((global.fetch as jest.Mock).mock.calls.length).toBe(callsAfterFill + 1);
169
+ });
170
+ });