@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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.retentionFloor = exports.requireQuota = exports.requireEntitlement = exports.loadEntitlements = exports.clearEntitlementsCache = exports.invalidateEntitlements = exports.getEntitlements = exports.setUsage = exports.releaseQuota = exports.consumeQuota = exports.checkQuota = exports.getUsageSnapshot = exports.getUsage = exports.currentPeriod = exports.UsageMeter = exports.resetEntitlementsModeWarning = exports.getEntitlementsMode = exports.fitsWithin = exports.isFeatureKey = exports.isMeterKey = exports.meterPeriod = exports.normalizeLimits = exports.resolveLimits = exports.ENTITLEMENT_CATALOG = exports.DEFAULT_PLAN_LIMITS = exports.FEATURE_DEFINITIONS = exports.METER_DEFINITIONS = exports.FEATURE_KEYS = exports.METER_KEYS = exports.applyLimits = exports.STAFF_PLAN = exports.FALLBACK_PLAN = exports.UNLIMITED = void 0;
|
|
4
|
+
var definitions_1 = require("./definitions");
|
|
5
|
+
Object.defineProperty(exports, "UNLIMITED", { enumerable: true, get: function () { return definitions_1.UNLIMITED; } });
|
|
6
|
+
Object.defineProperty(exports, "FALLBACK_PLAN", { enumerable: true, get: function () { return definitions_1.FALLBACK_PLAN; } });
|
|
7
|
+
Object.defineProperty(exports, "STAFF_PLAN", { enumerable: true, get: function () { return definitions_1.STAFF_PLAN; } });
|
|
8
|
+
Object.defineProperty(exports, "applyLimits", { enumerable: true, get: function () { return definitions_1.applyLimits; } });
|
|
9
|
+
Object.defineProperty(exports, "METER_KEYS", { enumerable: true, get: function () { return definitions_1.METER_KEYS; } });
|
|
10
|
+
Object.defineProperty(exports, "FEATURE_KEYS", { enumerable: true, get: function () { return definitions_1.FEATURE_KEYS; } });
|
|
11
|
+
Object.defineProperty(exports, "METER_DEFINITIONS", { enumerable: true, get: function () { return definitions_1.METER_DEFINITIONS; } });
|
|
12
|
+
Object.defineProperty(exports, "FEATURE_DEFINITIONS", { enumerable: true, get: function () { return definitions_1.FEATURE_DEFINITIONS; } });
|
|
13
|
+
Object.defineProperty(exports, "DEFAULT_PLAN_LIMITS", { enumerable: true, get: function () { return definitions_1.DEFAULT_PLAN_LIMITS; } });
|
|
14
|
+
Object.defineProperty(exports, "ENTITLEMENT_CATALOG", { enumerable: true, get: function () { return definitions_1.ENTITLEMENT_CATALOG; } });
|
|
15
|
+
Object.defineProperty(exports, "resolveLimits", { enumerable: true, get: function () { return definitions_1.resolveLimits; } });
|
|
16
|
+
Object.defineProperty(exports, "normalizeLimits", { enumerable: true, get: function () { return definitions_1.normalizeLimits; } });
|
|
17
|
+
Object.defineProperty(exports, "meterPeriod", { enumerable: true, get: function () { return definitions_1.meterPeriod; } });
|
|
18
|
+
Object.defineProperty(exports, "isMeterKey", { enumerable: true, get: function () { return definitions_1.isMeterKey; } });
|
|
19
|
+
Object.defineProperty(exports, "isFeatureKey", { enumerable: true, get: function () { return definitions_1.isFeatureKey; } });
|
|
20
|
+
Object.defineProperty(exports, "fitsWithin", { enumerable: true, get: function () { return definitions_1.fitsWithin; } });
|
|
21
|
+
var mode_1 = require("./mode");
|
|
22
|
+
Object.defineProperty(exports, "getEntitlementsMode", { enumerable: true, get: function () { return mode_1.getEntitlementsMode; } });
|
|
23
|
+
Object.defineProperty(exports, "resetEntitlementsModeWarning", { enumerable: true, get: function () { return mode_1.resetEntitlementsModeWarning; } });
|
|
24
|
+
var UsageMeter_1 = require("./UsageMeter");
|
|
25
|
+
Object.defineProperty(exports, "UsageMeter", { enumerable: true, get: function () { return UsageMeter_1.UsageMeter; } });
|
|
26
|
+
Object.defineProperty(exports, "currentPeriod", { enumerable: true, get: function () { return UsageMeter_1.currentPeriod; } });
|
|
27
|
+
var usage_1 = require("./usage");
|
|
28
|
+
Object.defineProperty(exports, "getUsage", { enumerable: true, get: function () { return usage_1.getUsage; } });
|
|
29
|
+
Object.defineProperty(exports, "getUsageSnapshot", { enumerable: true, get: function () { return usage_1.getUsageSnapshot; } });
|
|
30
|
+
Object.defineProperty(exports, "checkQuota", { enumerable: true, get: function () { return usage_1.checkQuota; } });
|
|
31
|
+
Object.defineProperty(exports, "consumeQuota", { enumerable: true, get: function () { return usage_1.consumeQuota; } });
|
|
32
|
+
Object.defineProperty(exports, "releaseQuota", { enumerable: true, get: function () { return usage_1.releaseQuota; } });
|
|
33
|
+
Object.defineProperty(exports, "setUsage", { enumerable: true, get: function () { return usage_1.setUsage; } });
|
|
34
|
+
var client_1 = require("./client");
|
|
35
|
+
Object.defineProperty(exports, "getEntitlements", { enumerable: true, get: function () { return client_1.getEntitlements; } });
|
|
36
|
+
Object.defineProperty(exports, "invalidateEntitlements", { enumerable: true, get: function () { return client_1.invalidateEntitlements; } });
|
|
37
|
+
Object.defineProperty(exports, "clearEntitlementsCache", { enumerable: true, get: function () { return client_1.clearEntitlementsCache; } });
|
|
38
|
+
var middleware_1 = require("./middleware");
|
|
39
|
+
Object.defineProperty(exports, "loadEntitlements", { enumerable: true, get: function () { return middleware_1.loadEntitlements; } });
|
|
40
|
+
Object.defineProperty(exports, "requireEntitlement", { enumerable: true, get: function () { return middleware_1.requireEntitlement; } });
|
|
41
|
+
Object.defineProperty(exports, "requireQuota", { enumerable: true, get: function () { return middleware_1.requireQuota; } });
|
|
42
|
+
Object.defineProperty(exports, "retentionFloor", { enumerable: true, get: function () { return middleware_1.retentionFloor; } });
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/entitlements/index.ts"],"names":[],"mappings":";;;AAAA,6CAiBuB;AAhBrB,wGAAA,SAAS,OAAA;AACT,4GAAA,aAAa,OAAA;AACb,yGAAA,UAAU,OAAA;AACV,0GAAA,WAAW,OAAA;AACX,yGAAA,UAAU,OAAA;AACV,2GAAA,YAAY,OAAA;AACZ,gHAAA,iBAAiB,OAAA;AACjB,kHAAA,mBAAmB,OAAA;AACnB,kHAAA,mBAAmB,OAAA;AACnB,kHAAA,mBAAmB,OAAA;AACnB,4GAAA,aAAa,OAAA;AACb,8GAAA,eAAe,OAAA;AACf,0GAAA,WAAW,OAAA;AACX,yGAAA,UAAU,OAAA;AACV,2GAAA,YAAY,OAAA;AACZ,yGAAA,UAAU,OAAA;AAaZ,+BAA2E;AAAlE,2GAAA,mBAAmB,OAAA;AAAE,oHAAA,4BAA4B,OAAA;AAE1D,2CAAyD;AAAhD,wGAAA,UAAU,OAAA;AAAE,2GAAA,aAAa,OAAA;AAElC,iCAAuG;AAA9F,iGAAA,QAAQ,OAAA;AAAE,yGAAA,gBAAgB,OAAA;AAAE,mGAAA,UAAU,OAAA;AAAE,qGAAA,YAAY,OAAA;AAAE,qGAAA,YAAY,OAAA;AAAE,iGAAA,QAAQ,OAAA;AAGrF,mCAA2F;AAAlF,yGAAA,eAAe,OAAA;AAAE,gHAAA,sBAAsB,OAAA;AAAE,gHAAA,sBAAsB,OAAA;AAExE,2CAAkG;AAAzF,8GAAA,gBAAgB,OAAA;AAAE,gHAAA,kBAAkB,OAAA;AAAE,0GAAA,YAAY,OAAA;AAAE,4GAAA,cAAc,OAAA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Request, RequestHandler } from 'express';
|
|
2
|
+
import { FeatureKey, MeterKey } from './definitions';
|
|
3
|
+
import { Entitlements } from './types';
|
|
4
|
+
declare global {
|
|
5
|
+
namespace Express {
|
|
6
|
+
interface Request {
|
|
7
|
+
/** Populated by the entitlement middlewares so controllers can read limits without a second lookup. */
|
|
8
|
+
entitlements?: Entitlements;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Who the check is billed to. Defaults to the authenticated user; group
|
|
14
|
+
* chokepoints override it with the group owner, since pooled quota belongs to
|
|
15
|
+
* whoever pays for the group.
|
|
16
|
+
*/
|
|
17
|
+
export type SubjectResolver = (req: Request) => string | undefined | Promise<string | undefined>;
|
|
18
|
+
export interface EntitlementOptions {
|
|
19
|
+
subject?: SubjectResolver;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolves and caches entitlements on the request. Two checks on one route
|
|
23
|
+
* (a feature gate plus a quota gate) therefore cost one lookup, not two.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadEntitlements(req: Request, resolve?: SubjectResolver): Promise<Entitlements>;
|
|
26
|
+
/**
|
|
27
|
+
* Gate a feature flag. Reads are never gated by design — use this on writes,
|
|
28
|
+
* dispatches and exports only.
|
|
29
|
+
*/
|
|
30
|
+
export declare function requireEntitlement(feature: FeatureKey, options?: EntitlementOptions): RequestHandler;
|
|
31
|
+
export interface QuotaOptions extends EntitlementOptions {
|
|
32
|
+
/**
|
|
33
|
+
* How much this request costs. A function when the amount is in the payload
|
|
34
|
+
* — an upload's `sizeBytes`, say — which is also why the storage gate sits on
|
|
35
|
+
* the signed-URL request: it is the only point where the size is known
|
|
36
|
+
* *before* the bytes are written.
|
|
37
|
+
*/
|
|
38
|
+
amount?: number | ((req: Request) => number);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read-only quota gate. Records nothing — the increment belongs at the point
|
|
42
|
+
* the resource actually comes into existence, via `consumeQuota`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function requireQuota(meter: MeterKey, options?: QuotaOptions): RequestHandler;
|
|
45
|
+
/**
|
|
46
|
+
* The oldest timestamp a user may read, or `null` for unlimited history.
|
|
47
|
+
*
|
|
48
|
+
* Meant to be dropped straight into an aggregation `$match` — this is what
|
|
49
|
+
* gates the portfolio planner, since the planner runs entirely client-side and
|
|
50
|
+
* any `<PaywallGate>` around it is bypassable from devtools. Truncating the
|
|
51
|
+
* price history it depends on is not.
|
|
52
|
+
*/
|
|
53
|
+
export declare function retentionFloor(email: string, now?: Date): Promise<Date | null>;
|
|
54
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/entitlements/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAa,MAAM,eAAe,CAAC;AAIhE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,uGAAuG;YACvG,YAAY,CAAC,EAAE,YAAY,CAAC;SAC7B;KACF;CACF;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjG,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAID;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,eAAgC,GAAG,OAAO,CAAC,YAAY,CAAC,CASrH;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,GAAE,kBAAuB,GAAG,cAAc,CA+BxG;AAED,MAAM,WAAW,YAAa,SAAQ,kBAAkB;IACtD;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;CAC9C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,GAAE,YAAiB,GAAG,cAAc,CAoBxF;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAQhG"}
|
|
@@ -0,0 +1,109 @@
|
|
|
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.loadEntitlements = loadEntitlements;
|
|
7
|
+
exports.requireEntitlement = requireEntitlement;
|
|
8
|
+
exports.requireQuota = requireQuota;
|
|
9
|
+
exports.retentionFloor = retentionFloor;
|
|
10
|
+
const logger_1 = __importDefault(require("../logging/logger"));
|
|
11
|
+
const HttpError_1 = require("../errors/HttpError");
|
|
12
|
+
const definitions_1 = require("./definitions");
|
|
13
|
+
const client_1 = require("./client");
|
|
14
|
+
const usage_1 = require("./usage");
|
|
15
|
+
const mode_1 = require("./mode");
|
|
16
|
+
const defaultSubject = req => req.user?.email;
|
|
17
|
+
/**
|
|
18
|
+
* Resolves and caches entitlements on the request. Two checks on one route
|
|
19
|
+
* (a feature gate plus a quota gate) therefore cost one lookup, not two.
|
|
20
|
+
*/
|
|
21
|
+
async function loadEntitlements(req, resolve = defaultSubject) {
|
|
22
|
+
if (req.entitlements)
|
|
23
|
+
return req.entitlements;
|
|
24
|
+
const subject = await resolve(req);
|
|
25
|
+
if (!subject)
|
|
26
|
+
throw new HttpError_1.UnauthorizedError('Authentication required');
|
|
27
|
+
const entitlements = await (0, client_1.getEntitlements)(subject);
|
|
28
|
+
req.entitlements = entitlements;
|
|
29
|
+
return entitlements;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Gate a feature flag. Reads are never gated by design — use this on writes,
|
|
33
|
+
* dispatches and exports only.
|
|
34
|
+
*/
|
|
35
|
+
function requireEntitlement(feature, options = {}) {
|
|
36
|
+
return async (req, _res, next) => {
|
|
37
|
+
try {
|
|
38
|
+
const mode = (0, mode_1.getEntitlementsMode)();
|
|
39
|
+
if (mode === 'off')
|
|
40
|
+
return next();
|
|
41
|
+
const entitlements = await loadEntitlements(req, options.subject);
|
|
42
|
+
if (entitlements.limits.features[feature])
|
|
43
|
+
return next();
|
|
44
|
+
if (mode === 'observe') {
|
|
45
|
+
logger_1.default.info('entitlement.would_block', {
|
|
46
|
+
event: 'entitlement.would_block',
|
|
47
|
+
feature,
|
|
48
|
+
subjectId: entitlements.email,
|
|
49
|
+
planCode: entitlements.planCode,
|
|
50
|
+
route: req.originalUrl,
|
|
51
|
+
stale: entitlements.stale === true
|
|
52
|
+
});
|
|
53
|
+
return next();
|
|
54
|
+
}
|
|
55
|
+
throw new HttpError_1.PaymentRequiredError(`Your plan does not include ${feature}`, {
|
|
56
|
+
code: 'FEATURE_NOT_IN_PLAN',
|
|
57
|
+
feature,
|
|
58
|
+
planCode: entitlements.planCode,
|
|
59
|
+
upgradeTo: entitlements.upgradeTo
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
next(err);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read-only quota gate. Records nothing — the increment belongs at the point
|
|
69
|
+
* the resource actually comes into existence, via `consumeQuota`.
|
|
70
|
+
*/
|
|
71
|
+
function requireQuota(meter, options = {}) {
|
|
72
|
+
return async (req, _res, next) => {
|
|
73
|
+
try {
|
|
74
|
+
if ((0, mode_1.getEntitlementsMode)() === 'off')
|
|
75
|
+
return next();
|
|
76
|
+
const entitlements = await loadEntitlements(req, options.subject);
|
|
77
|
+
const amount = typeof options.amount === 'function' ? options.amount(req) : (options.amount ?? 1);
|
|
78
|
+
await (0, usage_1.checkQuota)({
|
|
79
|
+
subjectId: entitlements.email,
|
|
80
|
+
meter,
|
|
81
|
+
amount,
|
|
82
|
+
entitlements,
|
|
83
|
+
route: req.originalUrl
|
|
84
|
+
});
|
|
85
|
+
next();
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
next(err);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The oldest timestamp a user may read, or `null` for unlimited history.
|
|
94
|
+
*
|
|
95
|
+
* Meant to be dropped straight into an aggregation `$match` — this is what
|
|
96
|
+
* gates the portfolio planner, since the planner runs entirely client-side and
|
|
97
|
+
* any `<PaywallGate>` around it is bypassable from devtools. Truncating the
|
|
98
|
+
* price history it depends on is not.
|
|
99
|
+
*/
|
|
100
|
+
async function retentionFloor(email, now = new Date()) {
|
|
101
|
+
if ((0, mode_1.getEntitlementsMode)() !== 'enforce')
|
|
102
|
+
return null;
|
|
103
|
+
const { limits } = await (0, client_1.getEntitlements)(email);
|
|
104
|
+
const days = limits.retentionDays;
|
|
105
|
+
if (days === definitions_1.UNLIMITED || days < 0)
|
|
106
|
+
return null;
|
|
107
|
+
return new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/entitlements/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EntitlementsMode } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Read per call rather than cached at import time: a service can be flipped to
|
|
4
|
+
* `enforce` by restarting it with a new env value, and tests set the variable
|
|
5
|
+
* between cases. The cost is one `process.env` lookup per check.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getEntitlementsMode(): EntitlementsMode;
|
|
8
|
+
/** Test seam — the "already warned" latch would otherwise leak between cases. */
|
|
9
|
+
export declare function resetEntitlementsModeWarning(): void;
|
|
10
|
+
//# sourceMappingURL=mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mode.d.ts","sourceRoot":"","sources":["../../src/entitlements/mode.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAM3C;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,gBAAgB,CAUtD;AAED,iFAAiF;AACjF,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
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.getEntitlementsMode = getEntitlementsMode;
|
|
7
|
+
exports.resetEntitlementsModeWarning = resetEntitlementsModeWarning;
|
|
8
|
+
const logger_1 = __importDefault(require("../logging/logger"));
|
|
9
|
+
const VALID_MODES = ['off', 'observe', 'enforce'];
|
|
10
|
+
let warnedAboutInvalidMode = false;
|
|
11
|
+
/**
|
|
12
|
+
* Read per call rather than cached at import time: a service can be flipped to
|
|
13
|
+
* `enforce` by restarting it with a new env value, and tests set the variable
|
|
14
|
+
* between cases. The cost is one `process.env` lookup per check.
|
|
15
|
+
*/
|
|
16
|
+
function getEntitlementsMode() {
|
|
17
|
+
const raw = (process.env.ENTITLEMENTS_MODE || '').trim().toLowerCase();
|
|
18
|
+
if (!raw)
|
|
19
|
+
return 'observe';
|
|
20
|
+
if (VALID_MODES.includes(raw))
|
|
21
|
+
return raw;
|
|
22
|
+
if (!warnedAboutInvalidMode) {
|
|
23
|
+
warnedAboutInvalidMode = true;
|
|
24
|
+
logger_1.default.warn('Invalid ENTITLEMENTS_MODE, falling back to observe', { value: raw });
|
|
25
|
+
}
|
|
26
|
+
return 'observe';
|
|
27
|
+
}
|
|
28
|
+
/** Test seam — the "already warned" latch would otherwise leak between cases. */
|
|
29
|
+
function resetEntitlementsModeWarning() {
|
|
30
|
+
warnedAboutInvalidMode = false;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mode.js","sourceRoot":"","sources":["../../src/entitlements/mode.ts"],"names":[],"mappings":";;;;;AAYA,kDAUC;AAGD,oEAEC;AA3BD,+DAAuC;AAGvC,MAAM,WAAW,GAAgC,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AAE/E,IAAI,sBAAsB,GAAG,KAAK,CAAC;AAEnC;;;;GAIG;AACH,SAAgB,mBAAmB;IACjC,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACvE,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,IAAK,WAAiC,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,GAAuB,CAAC;IAErF,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAC5B,sBAAsB,GAAG,IAAI,CAAC;QAC9B,gBAAM,CAAC,IAAI,CAAC,oDAAoD,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,iFAAiF;AACjF,SAAgB,4BAA4B;IAC1C,sBAAsB,GAAG,KAAK,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PlanLimits } from './definitions';
|
|
2
|
+
/**
|
|
3
|
+
* What payment-service answers for a user, and what the client caches.
|
|
4
|
+
* `limits` is already normalized — consumers never merge defaults themselves.
|
|
5
|
+
*/
|
|
6
|
+
export interface Entitlements {
|
|
7
|
+
email: string;
|
|
8
|
+
planCode: string;
|
|
9
|
+
/** Subscription status backing the plan; `null` for users on the implicit free plan. */
|
|
10
|
+
status: string | null;
|
|
11
|
+
limits: PlanLimits;
|
|
12
|
+
/** Cheapest active plan priced above the current one, for the upgrade dialog's CTA. */
|
|
13
|
+
upgradeTo?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Set when payment-service could not be reached and these are fallback
|
|
16
|
+
* limits rather than the user's real ones. Enforcement still applies (fail
|
|
17
|
+
* closed), but the value lets callers log the difference.
|
|
18
|
+
*/
|
|
19
|
+
stale?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `off` — no metering, no blocking. `observe` — record usage and log what
|
|
23
|
+
* would have been blocked, block nothing. `enforce` — block.
|
|
24
|
+
*
|
|
25
|
+
* Observe is the default because the configured limits start as guesses; two
|
|
26
|
+
* weeks of `entitlement.would_block` volume is what turns them into numbers.
|
|
27
|
+
*/
|
|
28
|
+
export type EntitlementsMode = 'off' | 'observe' | 'enforce';
|
|
29
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/entitlements/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/entitlements/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { MeterKey } from './definitions';
|
|
2
|
+
import { Entitlements } from './types';
|
|
3
|
+
export interface QuotaRequest {
|
|
4
|
+
/** Who the usage is billed to — see `IUsageMeter.subjectId`. */
|
|
5
|
+
subjectId: string;
|
|
6
|
+
meter: MeterKey;
|
|
7
|
+
/** How much this request wants. Bytes for `storageBytes`, otherwise a count. */
|
|
8
|
+
amount?: number;
|
|
9
|
+
entitlements: Entitlements;
|
|
10
|
+
/** Attached to the observe-mode log line so a would-block can be traced to a route. */
|
|
11
|
+
route?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Current count for one meter in its live period. */
|
|
14
|
+
export declare function getUsage(subjectId: string, meter: MeterKey, now?: Date): Promise<number>;
|
|
15
|
+
/** Every meter at once, for the account page and the pricing table's live numbers. */
|
|
16
|
+
export declare function getUsageSnapshot(subjectId: string, now?: Date): Promise<Record<MeterKey, number>>;
|
|
17
|
+
/**
|
|
18
|
+
* Read-only check for cumulative meters whose real increment happens later
|
|
19
|
+
* (storage is counted when the upload lands, not when it is authorized).
|
|
20
|
+
*
|
|
21
|
+
* Racy by construction — two concurrent requests can both pass — which is
|
|
22
|
+
* acceptable here precisely because the meters it guards are reconciled from
|
|
23
|
+
* the underlying data. Use `consumeQuota` where the count is the only record.
|
|
24
|
+
*/
|
|
25
|
+
export declare function checkQuota(req: QuotaRequest): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Atomically reserve capacity, throwing when the reservation would overrun.
|
|
28
|
+
*
|
|
29
|
+
* The increment and the check are one `$inc` + read, not a read-then-write:
|
|
30
|
+
* two concurrent requests that both read "24 used" against a limit of 25 would
|
|
31
|
+
* both proceed and overrun. Incrementing first and rejecting on the
|
|
32
|
+
* *post-increment* value means the loser of that race is the one rejected.
|
|
33
|
+
*
|
|
34
|
+
* A rejected reservation rolls its own increment back before throwing — unlike
|
|
35
|
+
* the provider-quota version this generalizes, where an over-limit count just
|
|
36
|
+
* sits there until the day rolls over. A per-user cumulative meter never rolls
|
|
37
|
+
* over, so a leaked increment is permanent and shows up months later as a user
|
|
38
|
+
* who cannot upload despite being well under their limit.
|
|
39
|
+
*/
|
|
40
|
+
export declare function consumeQuota(req: QuotaRequest): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Hand back capacity after a failure that never consumed it (the upload errored,
|
|
43
|
+
* the AI service refused the job), and on deletes — a cumulative meter that only
|
|
44
|
+
* counts up is a bug that surfaces as an angry support email six months in.
|
|
45
|
+
*
|
|
46
|
+
* Never drops below zero, and never throws: a failed release must not turn a
|
|
47
|
+
* recoverable error into a 500 for the caller that was already unwinding.
|
|
48
|
+
*/
|
|
49
|
+
export declare function releaseQuota(subjectId: string, meter: MeterKey, amount?: number, now?: Date): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Set a meter to a known value. For the backfill (existing storage has to be
|
|
52
|
+
* seeded or every current user starts at zero and silently exceeds later) and
|
|
53
|
+
* for periodic reconciliation against the source of truth.
|
|
54
|
+
*/
|
|
55
|
+
export declare function setUsage(subjectId: string, meter: MeterKey, count: number, now?: Date): Promise<void>;
|
|
56
|
+
//# sourceMappingURL=usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/entitlements/usage.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAqC,MAAM,eAAe,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,QAAQ,CAAC;IAChB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,YAAY,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,sDAAsD;AACtD,wBAAsB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAO9F;AAED,sFAAsF;AACtF,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAYvG;AA2BD;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAejE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BnE;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,QAAQ,EACf,MAAM,SAAI,EACV,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE,MAAM,EACb,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
|
@@ -0,0 +1,159 @@
|
|
|
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.getUsage = getUsage;
|
|
7
|
+
exports.getUsageSnapshot = getUsageSnapshot;
|
|
8
|
+
exports.checkQuota = checkQuota;
|
|
9
|
+
exports.consumeQuota = consumeQuota;
|
|
10
|
+
exports.releaseQuota = releaseQuota;
|
|
11
|
+
exports.setUsage = setUsage;
|
|
12
|
+
const logger_1 = __importDefault(require("../logging/logger"));
|
|
13
|
+
const HttpError_1 = require("../errors/HttpError");
|
|
14
|
+
const UsageMeter_1 = require("./UsageMeter");
|
|
15
|
+
const definitions_1 = require("./definitions");
|
|
16
|
+
const mode_1 = require("./mode");
|
|
17
|
+
/** Current count for one meter in its live period. */
|
|
18
|
+
async function getUsage(subjectId, meter, now) {
|
|
19
|
+
const doc = await UsageMeter_1.UsageMeter.findOne({
|
|
20
|
+
subjectId: subjectId.toLowerCase(),
|
|
21
|
+
meter,
|
|
22
|
+
period: (0, UsageMeter_1.currentPeriod)(meter, now)
|
|
23
|
+
}).lean();
|
|
24
|
+
return doc?.count ?? 0;
|
|
25
|
+
}
|
|
26
|
+
/** Every meter at once, for the account page and the pricing table's live numbers. */
|
|
27
|
+
async function getUsageSnapshot(subjectId, now) {
|
|
28
|
+
const periods = definitions_1.METER_KEYS.map(meter => ({ meter, period: (0, UsageMeter_1.currentPeriod)(meter, now) }));
|
|
29
|
+
const docs = await UsageMeter_1.UsageMeter.find({
|
|
30
|
+
subjectId: subjectId.toLowerCase(),
|
|
31
|
+
$or: periods.map(p => ({ meter: p.meter, period: p.period }))
|
|
32
|
+
}).lean();
|
|
33
|
+
const snapshot = Object.fromEntries(definitions_1.METER_KEYS.map(k => [k, 0]));
|
|
34
|
+
for (const doc of docs) {
|
|
35
|
+
if (definitions_1.METER_KEYS.includes(doc.meter))
|
|
36
|
+
snapshot[doc.meter] = doc.count;
|
|
37
|
+
}
|
|
38
|
+
return snapshot;
|
|
39
|
+
}
|
|
40
|
+
function quotaError(req, used, limit) {
|
|
41
|
+
return new HttpError_1.PaymentRequiredError(`Limit reached for ${req.meter}`, {
|
|
42
|
+
code: 'QUOTA_EXCEEDED',
|
|
43
|
+
meter: req.meter,
|
|
44
|
+
used,
|
|
45
|
+
limit,
|
|
46
|
+
planCode: req.entitlements.planCode,
|
|
47
|
+
upgradeTo: req.entitlements.upgradeTo
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function logWouldBlock(req, used, limit) {
|
|
51
|
+
logger_1.default.info('entitlement.would_block', {
|
|
52
|
+
event: 'entitlement.would_block',
|
|
53
|
+
meter: req.meter,
|
|
54
|
+
subjectId: req.subjectId,
|
|
55
|
+
amount: req.amount ?? 1,
|
|
56
|
+
used,
|
|
57
|
+
limit,
|
|
58
|
+
planCode: req.entitlements.planCode,
|
|
59
|
+
route: req.route,
|
|
60
|
+
stale: req.entitlements.stale === true
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read-only check for cumulative meters whose real increment happens later
|
|
65
|
+
* (storage is counted when the upload lands, not when it is authorized).
|
|
66
|
+
*
|
|
67
|
+
* Racy by construction — two concurrent requests can both pass — which is
|
|
68
|
+
* acceptable here precisely because the meters it guards are reconciled from
|
|
69
|
+
* the underlying data. Use `consumeQuota` where the count is the only record.
|
|
70
|
+
*/
|
|
71
|
+
async function checkQuota(req) {
|
|
72
|
+
const mode = (0, mode_1.getEntitlementsMode)();
|
|
73
|
+
if (mode === 'off')
|
|
74
|
+
return;
|
|
75
|
+
const limit = req.entitlements.limits.meters[req.meter] ?? definitions_1.UNLIMITED;
|
|
76
|
+
if (limit === definitions_1.UNLIMITED)
|
|
77
|
+
return;
|
|
78
|
+
const used = await getUsage(req.subjectId, req.meter);
|
|
79
|
+
if ((0, definitions_1.fitsWithin)(limit, used, req.amount ?? 1))
|
|
80
|
+
return;
|
|
81
|
+
if (mode === 'observe') {
|
|
82
|
+
logWouldBlock(req, used, limit);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw quotaError(req, used, limit);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Atomically reserve capacity, throwing when the reservation would overrun.
|
|
89
|
+
*
|
|
90
|
+
* The increment and the check are one `$inc` + read, not a read-then-write:
|
|
91
|
+
* two concurrent requests that both read "24 used" against a limit of 25 would
|
|
92
|
+
* both proceed and overrun. Incrementing first and rejecting on the
|
|
93
|
+
* *post-increment* value means the loser of that race is the one rejected.
|
|
94
|
+
*
|
|
95
|
+
* A rejected reservation rolls its own increment back before throwing — unlike
|
|
96
|
+
* the provider-quota version this generalizes, where an over-limit count just
|
|
97
|
+
* sits there until the day rolls over. A per-user cumulative meter never rolls
|
|
98
|
+
* over, so a leaked increment is permanent and shows up months later as a user
|
|
99
|
+
* who cannot upload despite being well under their limit.
|
|
100
|
+
*/
|
|
101
|
+
async function consumeQuota(req) {
|
|
102
|
+
const mode = (0, mode_1.getEntitlementsMode)();
|
|
103
|
+
if (mode === 'off')
|
|
104
|
+
return;
|
|
105
|
+
const amount = req.amount ?? 1;
|
|
106
|
+
const limit = req.entitlements.limits.meters[req.meter] ?? definitions_1.UNLIMITED;
|
|
107
|
+
const period = (0, UsageMeter_1.currentPeriod)(req.meter);
|
|
108
|
+
const subjectId = req.subjectId.toLowerCase();
|
|
109
|
+
const usage = await UsageMeter_1.UsageMeter.findOneAndUpdate({ subjectId, meter: req.meter, period }, { $inc: { count: amount } }, { upsert: true, returnDocument: 'after', setDefaultsOnInsert: true });
|
|
110
|
+
if (limit === definitions_1.UNLIMITED || usage.count <= limit)
|
|
111
|
+
return;
|
|
112
|
+
const usedBefore = usage.count - amount;
|
|
113
|
+
if (mode === 'observe') {
|
|
114
|
+
// Keep the increment: observe mode exists to measure real demand, and a
|
|
115
|
+
// rolled-back count would under-report exactly the users who matter.
|
|
116
|
+
logWouldBlock(req, usedBefore, limit);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await releaseQuota(subjectId, req.meter, amount);
|
|
120
|
+
throw quotaError(req, usedBefore, limit);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Hand back capacity after a failure that never consumed it (the upload errored,
|
|
124
|
+
* the AI service refused the job), and on deletes — a cumulative meter that only
|
|
125
|
+
* counts up is a bug that surfaces as an angry support email six months in.
|
|
126
|
+
*
|
|
127
|
+
* Never drops below zero, and never throws: a failed release must not turn a
|
|
128
|
+
* recoverable error into a 500 for the caller that was already unwinding.
|
|
129
|
+
*/
|
|
130
|
+
async function releaseQuota(subjectId, meter, amount = 1, now) {
|
|
131
|
+
try {
|
|
132
|
+
const filter = { subjectId: subjectId.toLowerCase(), meter, period: (0, UsageMeter_1.currentPeriod)(meter, now) };
|
|
133
|
+
// Conditional `$inc` rather than read-then-write: a read-modify-write here
|
|
134
|
+
// loses concurrent releases, which on a cumulative meter is permanent drift.
|
|
135
|
+
const result = await UsageMeter_1.UsageMeter.updateOne({ ...filter, count: { $gte: amount } }, { $inc: { count: -amount } });
|
|
136
|
+
if (result.matchedCount === 0) {
|
|
137
|
+
// Either no counter exists, or it holds less than we are handing back
|
|
138
|
+
// (a reconciliation reset it in between). Clamp at zero, never negative.
|
|
139
|
+
await UsageMeter_1.UsageMeter.updateOne({ ...filter, count: { $lt: amount } }, { $set: { count: 0 } });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
logger_1.default.warn('Failed to release usage quota', {
|
|
144
|
+
subjectId,
|
|
145
|
+
meter,
|
|
146
|
+
amount,
|
|
147
|
+
error: err?.message
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Set a meter to a known value. For the backfill (existing storage has to be
|
|
153
|
+
* seeded or every current user starts at zero and silently exceeds later) and
|
|
154
|
+
* for periodic reconciliation against the source of truth.
|
|
155
|
+
*/
|
|
156
|
+
async function setUsage(subjectId, meter, count, now) {
|
|
157
|
+
await UsageMeter_1.UsageMeter.updateOne({ subjectId: subjectId.toLowerCase(), meter, period: (0, UsageMeter_1.currentPeriod)(meter, now) }, { $set: { count: Math.max(0, Math.floor(count)) } }, { upsert: true, setDefaultsOnInsert: true });
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/entitlements/usage.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export declare class HttpError extends Error {
|
|
11
11
|
statusCode: number;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Extra fields merged into the JSON body by the shared errorHandler. Used by
|
|
14
|
+
* errors the client has to *act* on rather than just display — a 402 tells
|
|
15
|
+
* the upgrade dialog which meter blocked and which plan lifts it.
|
|
16
|
+
*/
|
|
17
|
+
details?: Record<string, unknown>;
|
|
18
|
+
constructor(statusCode: number, message: string, details?: Record<string, unknown>);
|
|
13
19
|
}
|
|
14
20
|
export declare class BadRequestError extends HttpError {
|
|
15
21
|
constructor(message?: string);
|
|
@@ -29,4 +35,27 @@ export declare class ConflictError extends HttpError {
|
|
|
29
35
|
export declare class TooManyRequestsError extends HttpError {
|
|
30
36
|
constructor(message?: string);
|
|
31
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Fields the upgrade dialog renders. Every one of them comes from the response
|
|
40
|
+
* body, so the dialog needs no knowledge of which call failed.
|
|
41
|
+
*/
|
|
42
|
+
export interface PaymentRequiredDetails {
|
|
43
|
+
/** Machine-readable reason, e.g. `QUOTA_EXCEEDED` or `FEATURE_NOT_IN_PLAN`. */
|
|
44
|
+
code: string;
|
|
45
|
+
meter?: string;
|
|
46
|
+
feature?: string;
|
|
47
|
+
used?: number;
|
|
48
|
+
limit?: number;
|
|
49
|
+
planCode?: string;
|
|
50
|
+
/** The cheapest plan that lifts this particular limit, when one exists. */
|
|
51
|
+
upgradeTo?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 402, never 403. 403 means "you may not"; 402 means "you may, for money" —
|
|
55
|
+
* and the frontend needs to tell those apart to choose between an error toast
|
|
56
|
+
* and an upgrade dialog.
|
|
57
|
+
*/
|
|
58
|
+
export declare class PaymentRequiredError extends HttpError {
|
|
59
|
+
constructor(message: string, details: PaymentRequiredDetails);
|
|
60
|
+
}
|
|
32
61
|
//# sourceMappingURL=HttpError.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpError.d.ts","sourceRoot":"","sources":["../../src/errors/HttpError.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,UAAU,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"HttpError.d.ts","sourceRoot":"","sources":["../../src/errors/HttpError.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAEtB,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASnF;AAED,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,SAAgB;CAGpC;AAED,qBAAa,iBAAkB,SAAQ,SAAS;gBAClC,OAAO,SAAiB;CAGrC;AAED,qBAAa,cAAe,SAAQ,SAAS;gBAC/B,OAAO,SAAkB;CAGtC;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAAc;CAGlC;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAAa;CAGjC;AAED,qBAAa,oBAAqB,SAAQ,SAAS;gBACrC,OAAO,SAAsB;CAG1C;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,SAAS;gBACrC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB;CAG7D"}
|