@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.
- package/dist/entitlements/UsageMeter.d.ts +30 -0
- package/dist/entitlements/UsageMeter.d.ts.map +1 -0
- package/dist/entitlements/UsageMeter.js +27 -0
- package/dist/entitlements/UsageMeter.js.map +1 -0
- package/dist/entitlements/client.d.ts +17 -0
- package/dist/entitlements/client.d.ts.map +1 -0
- package/dist/entitlements/client.js +142 -0
- package/dist/entitlements/client.js.map +1 -0
- package/dist/entitlements/definitions.d.ts +124 -0
- package/dist/entitlements/definitions.d.ts.map +1 -0
- package/dist/entitlements/definitions.js +210 -0
- package/dist/entitlements/definitions.js.map +1 -0
- package/dist/entitlements/index.d.ts +12 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +43 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/entitlements/middleware.d.ts +54 -0
- package/dist/entitlements/middleware.d.ts.map +1 -0
- package/dist/entitlements/middleware.js +109 -0
- package/dist/entitlements/middleware.js.map +1 -0
- package/dist/entitlements/mode.d.ts +10 -0
- package/dist/entitlements/mode.d.ts.map +1 -0
- package/dist/entitlements/mode.js +32 -0
- package/dist/entitlements/mode.js.map +1 -0
- package/dist/entitlements/types.d.ts +29 -0
- package/dist/entitlements/types.d.ts.map +1 -0
- package/dist/entitlements/types.js +3 -0
- package/dist/entitlements/types.js.map +1 -0
- package/dist/entitlements/usage.d.ts +56 -0
- package/dist/entitlements/usage.d.ts.map +1 -0
- package/dist/entitlements/usage.js +159 -0
- package/dist/entitlements/usage.js.map +1 -0
- package/dist/errors/HttpError.d.ts +30 -1
- package/dist/errors/HttpError.d.ts.map +1 -1
- package/dist/errors/HttpError.js +20 -2
- package/dist/errors/HttpError.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/corsMiddleware.d.ts.map +1 -1
- package/dist/middleware/corsMiddleware.js +5 -1
- package/dist/middleware/corsMiddleware.js.map +1 -1
- package/dist/middleware/errorHandler.d.ts.map +1 -1
- package/dist/middleware/errorHandler.js +4 -1
- package/dist/middleware/errorHandler.js.map +1 -1
- package/dist/middleware/internalServiceAuth.d.ts +44 -0
- package/dist/middleware/internalServiceAuth.d.ts.map +1 -0
- package/dist/middleware/internalServiceAuth.js +86 -0
- package/dist/middleware/internalServiceAuth.js.map +1 -0
- package/package.json +2 -1
- package/src/entitlements/UsageMeter.ts +49 -0
- package/src/entitlements/client.test.ts +170 -0
- package/src/entitlements/client.ts +162 -0
- package/src/entitlements/definitions.test.ts +161 -0
- package/src/entitlements/definitions.ts +261 -0
- package/src/entitlements/index.ts +40 -0
- package/src/entitlements/middleware.test.ts +179 -0
- package/src/entitlements/middleware.ts +136 -0
- package/src/entitlements/mode.test.ts +50 -0
- package/src/entitlements/mode.ts +28 -0
- package/src/entitlements/types.ts +30 -0
- package/src/entitlements/usage.test.ts +271 -0
- package/src/entitlements/usage.ts +189 -0
- package/src/errors/HttpError.test.ts +39 -1
- package/src/errors/HttpError.ts +35 -1
- package/src/index.ts +13 -1
- package/src/middleware/corsMiddleware.test.ts +38 -0
- package/src/middleware/corsMiddleware.ts +5 -1
- package/src/middleware/errorHandler.test.ts +43 -1
- package/src/middleware/errorHandler.ts +6 -1
- package/src/middleware/internalServiceAuth.test.ts +173 -0
- package/src/middleware/internalServiceAuth.ts +96 -0
package/dist/errors/HttpError.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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
|
|
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;
|
|
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
|
-
|
|
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,
|
|
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;
|
|
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":";;;;;;AACA,+DAAuC;AACvC,mDAAgD;
|
|
1
|
+
{"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":"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"}
|
|
@@ -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.
|
|
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
|
+
});
|