@tumbaland/backend-core 1.24.0 → 1.26.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/app/createBaseApp.d.ts +24 -0
- package/dist/app/createBaseApp.d.ts.map +1 -1
- package/dist/app/createBaseApp.js +20 -1
- package/dist/app/createBaseApp.js.map +1 -1
- package/dist/entitlements/client.d.ts.map +1 -1
- package/dist/entitlements/client.js +17 -1
- package/dist/entitlements/client.js.map +1 -1
- package/dist/entitlements/index.d.ts +3 -2
- package/dist/entitlements/index.d.ts.map +1 -1
- package/dist/entitlements/index.js +5 -4
- package/dist/entitlements/index.js.map +1 -1
- package/dist/entitlements/middleware.d.ts.map +1 -1
- package/dist/entitlements/middleware.js +8 -19
- package/dist/entitlements/middleware.js.map +1 -1
- package/dist/entitlements/reconcile.d.ts +74 -0
- package/dist/entitlements/reconcile.d.ts.map +1 -0
- package/dist/entitlements/reconcile.js +297 -0
- package/dist/entitlements/reconcile.js.map +1 -0
- package/dist/entitlements/types.d.ts +0 -8
- package/dist/entitlements/types.d.ts.map +1 -1
- package/dist/entitlements/usage.d.ts +1 -1
- package/dist/entitlements/usage.d.ts.map +1 -1
- package/dist/entitlements/usage.js +11 -20
- package/dist/entitlements/usage.js.map +1 -1
- package/dist/groups/client.d.ts +27 -0
- package/dist/groups/client.d.ts.map +1 -0
- package/dist/groups/client.js +153 -0
- package/dist/groups/client.js.map +1 -0
- package/dist/groups/index.d.ts +5 -0
- package/dist/groups/index.d.ts.map +1 -0
- package/dist/groups/index.js +10 -0
- package/dist/groups/index.js.map +1 -0
- package/dist/groups/subject.d.ts +30 -0
- package/dist/groups/subject.d.ts.map +1 -0
- package/dist/groups/subject.js +52 -0
- package/dist/groups/subject.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/app/createBaseApp.test.ts +31 -0
- package/src/app/createBaseApp.ts +28 -1
- package/src/entitlements/client.test.ts +30 -0
- package/src/entitlements/client.ts +18 -1
- package/src/entitlements/index.ts +4 -2
- package/src/entitlements/middleware.test.ts +9 -30
- package/src/entitlements/middleware.ts +8 -19
- package/src/entitlements/reconcile.test.ts +333 -0
- package/src/entitlements/reconcile.ts +384 -0
- package/src/entitlements/types.ts +0 -9
- package/src/entitlements/usage.test.ts +27 -31
- package/src/entitlements/usage.ts +12 -22
- package/src/groups/client.test.ts +215 -0
- package/src/groups/client.ts +182 -0
- package/src/groups/index.ts +5 -0
- package/src/groups/subject.test.ts +85 -0
- package/src/groups/subject.ts +50 -0
- package/src/index.ts +3 -0
- package/dist/entitlements/mode.d.ts +0 -10
- package/dist/entitlements/mode.d.ts.map +0 -1
- package/dist/entitlements/mode.js +0 -32
- package/dist/entitlements/mode.js.map +0 -1
- package/src/entitlements/mode.test.ts +0 -50
- package/src/entitlements/mode.ts +0 -28
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/groups/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.groupOwnerSubject = exports.clearGroupOwnerCache = exports.invalidateGroupOwner = exports.getGroupOwner = void 0;
|
|
4
|
+
var client_1 = require("./client");
|
|
5
|
+
Object.defineProperty(exports, "getGroupOwner", { enumerable: true, get: function () { return client_1.getGroupOwner; } });
|
|
6
|
+
Object.defineProperty(exports, "invalidateGroupOwner", { enumerable: true, get: function () { return client_1.invalidateGroupOwner; } });
|
|
7
|
+
Object.defineProperty(exports, "clearGroupOwnerCache", { enumerable: true, get: function () { return client_1.clearGroupOwnerCache; } });
|
|
8
|
+
var subject_1 = require("./subject");
|
|
9
|
+
Object.defineProperty(exports, "groupOwnerSubject", { enumerable: true, get: function () { return subject_1.groupOwnerSubject; } });
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/groups/index.ts"],"names":[],"mappings":";;;AAAA,mCAAqF;AAA5E,uGAAA,aAAa,OAAA;AAAE,8GAAA,oBAAoB,OAAA;AAAE,8GAAA,oBAAoB,OAAA;AAGlE,qCAA8C;AAArC,4GAAA,iBAAiB,OAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Request } from 'express';
|
|
2
|
+
import { SubjectResolver } from '../entitlements/middleware';
|
|
3
|
+
export type GroupIdResolver = (req: Request) => string | undefined | Promise<string | undefined>;
|
|
4
|
+
/**
|
|
5
|
+
* Builds the `subject` resolver that `requireQuota` / `requireEntitlement`
|
|
6
|
+
* already accept, for the meters whose resource can live in a group.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* requireQuota('storageBytes', {
|
|
10
|
+
* subject: groupOwnerSubject(req => req.body.groupId),
|
|
11
|
+
* amount: req => req.body.sizeBytes
|
|
12
|
+
* })
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Personal rows keep billing to the caller; group rows bill to the group's
|
|
16
|
+
* owner, because Family sells *pooled* storage and seats and the uploader's own
|
|
17
|
+
* free tier is not the pool.
|
|
18
|
+
*
|
|
19
|
+
* **When the owner cannot be resolved, it bills the caller and says so.** The
|
|
20
|
+
* alternative — refusing the write — is harsher than what the entitlement
|
|
21
|
+
* client itself does for the same class of outage (it degrades to free limits
|
|
22
|
+
* rather than denying), and the error direction here is bounded in the shape
|
|
23
|
+
* that matters: the common case is a member on a cheap personal plan uploading
|
|
24
|
+
* into a group owned by someone on Family, so falling back to the caller checks
|
|
25
|
+
* against the *smaller* allowance. It can over-grant when the caller happens to
|
|
26
|
+
* hold the richer plan, which is why it logs at `error` — a run of these means
|
|
27
|
+
* group-service is down, not that a limit is wrong.
|
|
28
|
+
*/
|
|
29
|
+
export declare function groupOwnerSubject(resolveGroupId: GroupIdResolver): SubjectResolver;
|
|
30
|
+
//# sourceMappingURL=subject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subject.d.ts","sourceRoot":"","sources":["../../src/groups/subject.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAG7D,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,CAAC,cAAc,EAAE,eAAe,GAAG,eAAe,CAiBlF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
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.groupOwnerSubject = groupOwnerSubject;
|
|
7
|
+
const logger_1 = __importDefault(require("../logging/logger"));
|
|
8
|
+
const client_1 = require("./client");
|
|
9
|
+
/**
|
|
10
|
+
* Builds the `subject` resolver that `requireQuota` / `requireEntitlement`
|
|
11
|
+
* already accept, for the meters whose resource can live in a group.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* requireQuota('storageBytes', {
|
|
15
|
+
* subject: groupOwnerSubject(req => req.body.groupId),
|
|
16
|
+
* amount: req => req.body.sizeBytes
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Personal rows keep billing to the caller; group rows bill to the group's
|
|
21
|
+
* owner, because Family sells *pooled* storage and seats and the uploader's own
|
|
22
|
+
* free tier is not the pool.
|
|
23
|
+
*
|
|
24
|
+
* **When the owner cannot be resolved, it bills the caller and says so.** The
|
|
25
|
+
* alternative — refusing the write — is harsher than what the entitlement
|
|
26
|
+
* client itself does for the same class of outage (it degrades to free limits
|
|
27
|
+
* rather than denying), and the error direction here is bounded in the shape
|
|
28
|
+
* that matters: the common case is a member on a cheap personal plan uploading
|
|
29
|
+
* into a group owned by someone on Family, so falling back to the caller checks
|
|
30
|
+
* against the *smaller* allowance. It can over-grant when the caller happens to
|
|
31
|
+
* hold the richer plan, which is why it logs at `error` — a run of these means
|
|
32
|
+
* group-service is down, not that a limit is wrong.
|
|
33
|
+
*/
|
|
34
|
+
function groupOwnerSubject(resolveGroupId) {
|
|
35
|
+
return async (req) => {
|
|
36
|
+
const caller = req.user?.email;
|
|
37
|
+
const groupId = await resolveGroupId(req);
|
|
38
|
+
if (!groupId)
|
|
39
|
+
return caller;
|
|
40
|
+
const owner = await (0, client_1.getGroupOwner)(String(groupId));
|
|
41
|
+
if (owner)
|
|
42
|
+
return owner.ownerEmail;
|
|
43
|
+
logger_1.default.error('Group owner unresolved; billing usage to the caller instead', {
|
|
44
|
+
event: 'entitlement.subject_fallback',
|
|
45
|
+
groupId: String(groupId),
|
|
46
|
+
caller,
|
|
47
|
+
route: req.originalUrl
|
|
48
|
+
});
|
|
49
|
+
return caller;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=subject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subject.js","sourceRoot":"","sources":["../../src/groups/subject.ts"],"names":[],"mappings":";;;;;AAgCA,8CAiBC;AAhDD,+DAAuC;AAEvC,qCAAyC;AAIzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,SAAgB,iBAAiB,CAAC,cAA+B;IAC/D,OAAO,KAAK,EAAE,GAAY,EAAE,EAAE;QAC5B,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC;QAC/B,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAI,CAAC,OAAO;YAAE,OAAO,MAAM,CAAC;QAE5B,MAAM,KAAK,GAAG,MAAM,IAAA,sBAAa,EAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QACnD,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC,UAAU,CAAC;QAEnC,gBAAM,CAAC,KAAK,CAAC,6DAA6D,EAAE;YAC1E,KAAK,EAAE,8BAA8B;YACrC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC;YACxB,MAAM;YACN,KAAK,EAAE,GAAG,CAAC,WAAW;SACvB,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { requireEnv } from './config/env';
|
|
|
11
11
|
export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, TooManyRequestsError, PaymentRequiredError } from './errors/HttpError';
|
|
12
12
|
export type { PaymentRequiredDetails } from './errors/HttpError';
|
|
13
13
|
export * from './entitlements';
|
|
14
|
+
export * from './groups';
|
|
14
15
|
export { authenticateToken } from './middleware/authMiddleware';
|
|
15
16
|
export { requireInternalServiceToken, internalServiceTokenBypass, allowUserOrInternalService, verifyInternalToken, INTERNAL_TOKEN_HEADER, SERVICE_ID_HEADER } from './middleware/internalServiceAuth';
|
|
16
17
|
export { createCorsMiddleware } from './middleware/corsMiddleware';
|
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,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"}
|
|
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,cAAc,UAAU,CAAC;AAGzB,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
|
@@ -54,6 +54,8 @@ Object.defineProperty(exports, "TooManyRequestsError", { enumerable: true, get:
|
|
|
54
54
|
Object.defineProperty(exports, "PaymentRequiredError", { enumerable: true, get: function () { return HttpError_1.PaymentRequiredError; } });
|
|
55
55
|
// Entitlements (plan limits, usage meters, enforcement)
|
|
56
56
|
__exportStar(require("./entitlements"), exports);
|
|
57
|
+
// Group ownership (the quota subject for anything pooled)
|
|
58
|
+
__exportStar(require("./groups"), exports);
|
|
57
59
|
// Middleware
|
|
58
60
|
var authMiddleware_1 = require("./middleware/authMiddleware");
|
|
59
61
|
Object.defineProperty(exports, "authenticateToken", { enumerable: true, get: function () { return authMiddleware_1.authenticateToken; } });
|
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,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
|
+
{"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,0DAA0D;AAC1D,2CAAyB;AAEzB,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"}
|
package/package.json
CHANGED
|
@@ -63,6 +63,37 @@ describe('createBaseApp', () => {
|
|
|
63
63
|
expect(res.status).toBe(200);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
it('trusts the proxy, so X-Forwarded-For identifies the client rather than being ignored', async () => {
|
|
67
|
+
const app = createBaseApp();
|
|
68
|
+
app.get('/whoami', (req, res) => res.json({ ip: req.ip }));
|
|
69
|
+
|
|
70
|
+
const res = await request(app).get('/whoami').set('X-Forwarded-For', '203.0.113.7');
|
|
71
|
+
|
|
72
|
+
expect(res.body.ip).toBe('203.0.113.7');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('keeps rate-limit buckets per client, not one shared bucket behind the proxy', async () => {
|
|
76
|
+
const app = createBaseApp({ rateLimiter: (await import('../middleware/security')).createRateLimiter({ windowMs: 60_000, max: 1 }) });
|
|
77
|
+
app.get('/ping', (_req, res) => res.json({ ok: true }));
|
|
78
|
+
|
|
79
|
+
const clientA = await request(app).get('/ping').set('X-Forwarded-For', '203.0.113.7');
|
|
80
|
+
const clientB = await request(app).get('/ping').set('X-Forwarded-For', '198.51.100.4');
|
|
81
|
+
const clientAAgain = await request(app).get('/ping').set('X-Forwarded-For', '203.0.113.7');
|
|
82
|
+
|
|
83
|
+
expect(clientA.status).toBe(200);
|
|
84
|
+
expect(clientB.status).toBe(200); // a different client is unaffected by A exhausting its window
|
|
85
|
+
expect(clientAAgain.status).toBe(429);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('ignores X-Forwarded-For when trustProxy is disabled', async () => {
|
|
89
|
+
const app = createBaseApp({ trustProxy: false });
|
|
90
|
+
app.get('/whoami', (req, res) => res.json({ ip: req.ip }));
|
|
91
|
+
|
|
92
|
+
const res = await request(app).get('/whoami').set('X-Forwarded-For', '203.0.113.7');
|
|
93
|
+
|
|
94
|
+
expect(res.body.ip).not.toBe('203.0.113.7');
|
|
95
|
+
});
|
|
96
|
+
|
|
66
97
|
it('mounts /health/live ahead of the rate limiter, so it never gets 429s a busy service would', async () => {
|
|
67
98
|
const app = createBaseApp({ rateLimiter: (await import('../middleware/security')).createRateLimiter({ windowMs: 60_000, max: 1 }) });
|
|
68
99
|
app.get('/ping', (_req, res) => res.json({ ok: true }));
|
package/src/app/createBaseApp.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface CreateBaseAppOptions {
|
|
|
25
25
|
tracing?: boolean;
|
|
26
26
|
/** Use requestLoggerWithMetrics (adds Prometheus histograms) instead of the plain requestLogger. Default true. */
|
|
27
27
|
metrics?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Value for Express's `trust proxy`. Defaults to trusting private/loopback
|
|
30
|
+
* addresses — see the note on tumbaland-proxy below. Pass `false` for a
|
|
31
|
+
* service exposed directly to clients with no proxy in front of it.
|
|
32
|
+
*/
|
|
33
|
+
trustProxy?: Parameters<Express['set']>[1];
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
@@ -47,6 +53,24 @@ export interface CreateBaseAppOptions {
|
|
|
47
53
|
* Services with non-standard body parsing (a webhook needing the raw body,
|
|
48
54
|
* or conditional parsing per-route) should pass `parseJson`/`parseCookies:
|
|
49
55
|
* false` and mount those themselves at the exact point they need to.
|
|
56
|
+
*
|
|
57
|
+
* `trust proxy` is on by default because nothing here is reached directly:
|
|
58
|
+
* every service sits behind tumbaland-proxy, which sets `X-Forwarded-For`.
|
|
59
|
+
* Left at Express's default (`false`) that header is ignored, which
|
|
60
|
+
* express-rate-limit reports as `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR` — and
|
|
61
|
+
* it is right to: the limiter would then key every request in the cluster on
|
|
62
|
+
* the proxy's own container IP, so one busy client could exhaust the window
|
|
63
|
+
* for everyone.
|
|
64
|
+
*
|
|
65
|
+
* The value is `loopback, linklocal, uniquelocal` (private ranges) rather
|
|
66
|
+
* than a hop count, because the chain length differs per ingress path: a
|
|
67
|
+
* direct hit arrives as `client` + the proxy's own socket address, while
|
|
68
|
+
* traffic through the Cloudflare tunnel arrives as `client, 127.0.0.1` (the
|
|
69
|
+
* local cloudflared) + that same socket address. Express walks the chain
|
|
70
|
+
* right-to-left and stops at the first address it does not trust, so both
|
|
71
|
+
* paths land on the real client; a fixed number would be wrong for one of
|
|
72
|
+
* them. All the intermediaries are private addresses and the client is not,
|
|
73
|
+
* which is exactly the split this preset encodes.
|
|
50
74
|
*/
|
|
51
75
|
export function createBaseApp(options: CreateBaseAppOptions = {}): Express {
|
|
52
76
|
const {
|
|
@@ -55,11 +79,14 @@ export function createBaseApp(options: CreateBaseAppOptions = {}): Express {
|
|
|
55
79
|
parseJson = true,
|
|
56
80
|
parseCookies = true,
|
|
57
81
|
tracing = true,
|
|
58
|
-
metrics = true
|
|
82
|
+
metrics = true,
|
|
83
|
+
trustProxy = 'loopback, linklocal, uniquelocal'
|
|
59
84
|
} = options;
|
|
60
85
|
|
|
61
86
|
const app = express();
|
|
62
87
|
|
|
88
|
+
app.set('trust proxy', trustProxy);
|
|
89
|
+
|
|
63
90
|
app.get('/health/live', healthLive);
|
|
64
91
|
|
|
65
92
|
app.use(securityHeaders);
|
|
@@ -150,6 +150,36 @@ describe('when payment-service cannot be reached', () => {
|
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
describe('base URL selection', () => {
|
|
154
|
+
it('prefers the container-network address over the public hostname', async () => {
|
|
155
|
+
process.env.PAYMENT_INTERNAL_URL = 'http://payment-service:5005';
|
|
156
|
+
process.env.PAYMENT_API_URL = 'https://payment-api.tumbaland.eu';
|
|
157
|
+
(global.fetch as jest.Mock).mockResolvedValue(paidResponse());
|
|
158
|
+
|
|
159
|
+
await getEntitlements('user@example.com');
|
|
160
|
+
|
|
161
|
+
// payment-front templates the public variable into the browser's
|
|
162
|
+
// config.json, so it cannot be pointed inward — hence the second variable.
|
|
163
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
164
|
+
'http://payment-service:5005/internal/entitlements?email=user%40example.com',
|
|
165
|
+
expect.anything()
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('falls back to the public URL, so local dev and un-migrated deploys still work', async () => {
|
|
170
|
+
delete process.env.PAYMENT_INTERNAL_URL;
|
|
171
|
+
process.env.PAYMENT_API_URL = 'https://payment-api.tumbaland.eu';
|
|
172
|
+
(global.fetch as jest.Mock).mockResolvedValue(paidResponse());
|
|
173
|
+
|
|
174
|
+
await getEntitlements('user@example.com');
|
|
175
|
+
|
|
176
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
177
|
+
'https://payment-api.tumbaland.eu/internal/entitlements?email=user%40example.com',
|
|
178
|
+
expect.anything()
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
153
183
|
describe('cache bounds', () => {
|
|
154
184
|
it('evicts the oldest entries instead of growing without limit', async () => {
|
|
155
185
|
(global.fetch as jest.Mock).mockResolvedValue(paidResponse());
|
|
@@ -72,8 +72,25 @@ interface EntitlementResponse {
|
|
|
72
72
|
upgradeTo?: string;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Prefers the container-network address over the public hostname.
|
|
77
|
+
*
|
|
78
|
+
* `PAYMENT_API_URL` cannot simply be pointed inward: payment-front's entrypoint
|
|
79
|
+
* templates that same variable into the `config.json` the *browser* downloads,
|
|
80
|
+
* so a Docker-internal value there breaks the subscription and checkout pages.
|
|
81
|
+
* One variable, two consumers with incompatible needs — this is the server-side
|
|
82
|
+
* half of it.
|
|
83
|
+
*
|
|
84
|
+
* Falling back keeps local dev (where only `PAYMENT_API_URL` is set) and any
|
|
85
|
+
* un-migrated deployment working; it just takes the long way round, out through
|
|
86
|
+
* the proxy and back, carrying the internal token across the public edge.
|
|
87
|
+
*/
|
|
88
|
+
function internalPaymentUrl(): string | undefined {
|
|
89
|
+
return process.env.PAYMENT_INTERNAL_URL || process.env.PAYMENT_API_URL;
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
async function fetchEntitlements(email: string): Promise<Entitlements> {
|
|
76
|
-
const baseUrl =
|
|
93
|
+
const baseUrl = internalPaymentUrl();
|
|
77
94
|
const token = process.env.INTERNAL_SERVICE_TOKEN;
|
|
78
95
|
const serviceId = process.env.SERVICE_NAME || 'unknown-service';
|
|
79
96
|
|
|
@@ -26,14 +26,16 @@ export type {
|
|
|
26
26
|
PartialPlanLimits
|
|
27
27
|
} from './definitions';
|
|
28
28
|
|
|
29
|
-
export type { Entitlements
|
|
30
|
-
export { getEntitlementsMode, resetEntitlementsModeWarning } from './mode';
|
|
29
|
+
export type { Entitlements } from './types';
|
|
31
30
|
|
|
32
31
|
export { UsageMeter, currentPeriod } from './UsageMeter';
|
|
33
32
|
export type { IUsageMeter } from './UsageMeter';
|
|
34
33
|
export { getUsage, getUsageSnapshot, checkQuota, consumeQuota, releaseQuota, setUsage } from './usage';
|
|
35
34
|
export type { QuotaRequest } from './usage';
|
|
36
35
|
|
|
36
|
+
export { reconcileUsageMeters, runUsageReconciliation, RECONCILED_METERS } from './reconcile';
|
|
37
|
+
export type { ReconcileOptions, ReconcileReport, MeterChange, OrphanedMeter } from './reconcile';
|
|
38
|
+
|
|
37
39
|
export { getEntitlements, invalidateEntitlements, clearEntitlementsCache } from './client';
|
|
38
40
|
|
|
39
41
|
export { loadEntitlements, requireEntitlement, requireQuota, retentionFloor } from './middleware';
|
|
@@ -35,7 +35,7 @@ function mockReq(overrides: Partial<Request> = {}): Request {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
beforeEach(() => {
|
|
38
|
-
process.env = { ...ORIGINAL_ENV
|
|
38
|
+
process.env = { ...ORIGINAL_ENV };
|
|
39
39
|
jest.clearAllMocks();
|
|
40
40
|
mockGetEntitlements.mockResolvedValue(entitlements());
|
|
41
41
|
});
|
|
@@ -89,24 +89,10 @@ describe('requireEntitlement', () => {
|
|
|
89
89
|
);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
it('logs
|
|
93
|
-
|
|
94
|
-
const next = jest.fn();
|
|
95
|
-
|
|
96
|
-
await requireEntitlement('cleanExport')(mockReq(), {} as Response, next);
|
|
97
|
-
|
|
98
|
-
expect(next).toHaveBeenCalledWith();
|
|
99
|
-
expect(logger.info).toHaveBeenCalledWith('entitlement.would_block', expect.objectContaining({ feature: 'cleanExport' }));
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('does not even look entitlements up when the mode is off', async () => {
|
|
103
|
-
process.env.ENTITLEMENTS_MODE = 'off';
|
|
104
|
-
const next = jest.fn();
|
|
105
|
-
|
|
106
|
-
await requireEntitlement('cleanExport')(mockReq(), {} as Response, next);
|
|
92
|
+
it('logs the denial alongside throwing it', async () => {
|
|
93
|
+
await requireEntitlement('cleanExport')(mockReq(), {} as Response, jest.fn());
|
|
107
94
|
|
|
108
|
-
expect(
|
|
109
|
-
expect(mockGetEntitlements).not.toHaveBeenCalled();
|
|
95
|
+
expect(logger.info).toHaveBeenCalledWith('entitlement.blocked', expect.objectContaining({ feature: 'cleanExport' }));
|
|
110
96
|
});
|
|
111
97
|
});
|
|
112
98
|
|
|
@@ -143,15 +129,6 @@ describe('requireQuota', () => {
|
|
|
143
129
|
expect(next).toHaveBeenCalledWith(denial);
|
|
144
130
|
});
|
|
145
131
|
|
|
146
|
-
it('is inert when the mode is off', async () => {
|
|
147
|
-
process.env.ENTITLEMENTS_MODE = 'off';
|
|
148
|
-
const next = jest.fn();
|
|
149
|
-
|
|
150
|
-
await requireQuota('storageBytes')(mockReq(), {} as Response, next);
|
|
151
|
-
|
|
152
|
-
expect(mockCheckQuota).not.toHaveBeenCalled();
|
|
153
|
-
expect(next).toHaveBeenCalledWith();
|
|
154
|
-
});
|
|
155
132
|
});
|
|
156
133
|
|
|
157
134
|
describe('retentionFloor', () => {
|
|
@@ -171,9 +148,11 @@ describe('retentionFloor', () => {
|
|
|
171
148
|
expect(await retentionFloor('user@example.com', now)).toBeNull();
|
|
172
149
|
});
|
|
173
150
|
|
|
174
|
-
it('
|
|
175
|
-
|
|
151
|
+
it('still applies the floor when the limits are a stale fallback', async () => {
|
|
152
|
+
mockGetEntitlements.mockResolvedValue(entitlements({ stale: true }));
|
|
176
153
|
|
|
177
|
-
|
|
154
|
+
// Fail closed, same as every other gate: an outage clips history to the
|
|
155
|
+
// free tier for a minute rather than handing the full archive out.
|
|
156
|
+
expect(await retentionFloor('user@example.com', now)).toEqual(new Date('2026-05-11T00:00:00.000Z'));
|
|
178
157
|
});
|
|
179
158
|
});
|
|
@@ -4,7 +4,6 @@ import { PaymentRequiredError, UnauthorizedError } from '../errors/HttpError';
|
|
|
4
4
|
import { FeatureKey, MeterKey, UNLIMITED } from './definitions';
|
|
5
5
|
import { getEntitlements } from './client';
|
|
6
6
|
import { checkQuota } from './usage';
|
|
7
|
-
import { getEntitlementsMode } from './mode';
|
|
8
7
|
import { Entitlements } from './types';
|
|
9
8
|
|
|
10
9
|
declare global {
|
|
@@ -51,23 +50,17 @@ export async function loadEntitlements(req: Request, resolve: SubjectResolver =
|
|
|
51
50
|
export function requireEntitlement(feature: FeatureKey, options: EntitlementOptions = {}): RequestHandler {
|
|
52
51
|
return async (req, _res, next) => {
|
|
53
52
|
try {
|
|
54
|
-
const mode = getEntitlementsMode();
|
|
55
|
-
if (mode === 'off') return next();
|
|
56
|
-
|
|
57
53
|
const entitlements = await loadEntitlements(req, options.subject);
|
|
58
54
|
if (entitlements.limits.features[feature]) return next();
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
69
|
-
return next();
|
|
70
|
-
}
|
|
56
|
+
logger.info('entitlement.blocked', {
|
|
57
|
+
event: 'entitlement.blocked',
|
|
58
|
+
feature,
|
|
59
|
+
subjectId: entitlements.email,
|
|
60
|
+
planCode: entitlements.planCode,
|
|
61
|
+
route: req.originalUrl,
|
|
62
|
+
stale: entitlements.stale === true
|
|
63
|
+
});
|
|
71
64
|
|
|
72
65
|
throw new PaymentRequiredError(`Your plan does not include ${feature}`, {
|
|
73
66
|
code: 'FEATURE_NOT_IN_PLAN',
|
|
@@ -98,8 +91,6 @@ export interface QuotaOptions extends EntitlementOptions {
|
|
|
98
91
|
export function requireQuota(meter: MeterKey, options: QuotaOptions = {}): RequestHandler {
|
|
99
92
|
return async (req, _res, next) => {
|
|
100
93
|
try {
|
|
101
|
-
if (getEntitlementsMode() === 'off') return next();
|
|
102
|
-
|
|
103
94
|
const entitlements = await loadEntitlements(req, options.subject);
|
|
104
95
|
const amount = typeof options.amount === 'function' ? options.amount(req) : (options.amount ?? 1);
|
|
105
96
|
|
|
@@ -126,8 +117,6 @@ export function requireQuota(meter: MeterKey, options: QuotaOptions = {}): Reque
|
|
|
126
117
|
* price history it depends on is not.
|
|
127
118
|
*/
|
|
128
119
|
export async function retentionFloor(email: string, now: Date = new Date()): Promise<Date | null> {
|
|
129
|
-
if (getEntitlementsMode() !== 'enforce') return null;
|
|
130
|
-
|
|
131
120
|
const { limits } = await getEntitlements(email);
|
|
132
121
|
const days = limits.retentionDays;
|
|
133
122
|
if (days === UNLIMITED || days < 0) return null;
|