@tumbaland/backend-core 1.23.0 → 1.25.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/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/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/app/createBaseApp.test.ts +31 -0
- package/src/app/createBaseApp.ts +28 -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/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,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The entitlement *vocabulary*: which meters and features exist, what they
|
|
4
|
+
* mean, and what a plan gets when nothing else says otherwise.
|
|
5
|
+
*
|
|
6
|
+
* The actual per-plan numbers are NOT here — they live on the `Plan` document
|
|
7
|
+
* in Mongo and are editable by an admin at runtime (see payment-service's
|
|
8
|
+
* `Plan.limits` and `PUT /api/plans/:code/limits`). Shipping a code change to
|
|
9
|
+
* move a limit from 50 GB to 75 GB would be absurd, and a hardcoded table
|
|
10
|
+
* would immediately drift from what the pricing page renders.
|
|
11
|
+
*
|
|
12
|
+
* What code still owns:
|
|
13
|
+
* - the set of valid keys, so a typo in an admin form can't invent a meter
|
|
14
|
+
* that nothing enforces (`normalizeLimits` drops unknown keys);
|
|
15
|
+
* - `DEFAULT_PLAN_LIMITS`, the values used when a Plan document has no
|
|
16
|
+
* `limits` yet (fresh install, newly created plan) or when payment-service
|
|
17
|
+
* is unreachable and the client has to fail closed;
|
|
18
|
+
* - `ENTITLEMENT_CATALOG`, the metadata an admin UI renders its form from,
|
|
19
|
+
* so adding a meter needs no frontend change.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.ENTITLEMENT_CATALOG = exports.FALLBACK_PLAN = exports.STAFF_PLAN = exports.DEFAULT_PLAN_LIMITS = exports.FEATURE_KEYS = exports.METER_KEYS = exports.FEATURE_DEFINITIONS = exports.METER_DEFINITIONS = exports.UNLIMITED = void 0;
|
|
23
|
+
exports.meterPeriod = meterPeriod;
|
|
24
|
+
exports.isMeterKey = isMeterKey;
|
|
25
|
+
exports.isFeatureKey = isFeatureKey;
|
|
26
|
+
exports.resolveLimits = resolveLimits;
|
|
27
|
+
exports.applyLimits = applyLimits;
|
|
28
|
+
exports.normalizeLimits = normalizeLimits;
|
|
29
|
+
exports.fitsWithin = fitsWithin;
|
|
30
|
+
/** Sentinel for "no ceiling". Chosen over `Infinity` because it survives JSON and BSON. */
|
|
31
|
+
exports.UNLIMITED = -1;
|
|
32
|
+
exports.METER_DEFINITIONS = [
|
|
33
|
+
{
|
|
34
|
+
key: 'storageBytes',
|
|
35
|
+
label: 'Storage',
|
|
36
|
+
unit: 'bytes',
|
|
37
|
+
period: 'ALL',
|
|
38
|
+
description: 'Total bytes of uploaded photos and files held for the user.'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'aiJobs',
|
|
42
|
+
label: 'AI analyses',
|
|
43
|
+
unit: 'count',
|
|
44
|
+
period: 'MONTH',
|
|
45
|
+
description: 'Segmentation and object-detection jobs dispatched per calendar month.'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: 'trackedTickers',
|
|
49
|
+
label: 'Tracked tickers',
|
|
50
|
+
unit: 'count',
|
|
51
|
+
period: 'ALL',
|
|
52
|
+
description: 'Instruments followed for price history. Each one spends the deployment-wide market-data budget.'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'seats',
|
|
56
|
+
label: 'Group seats',
|
|
57
|
+
unit: 'count',
|
|
58
|
+
period: 'ALL',
|
|
59
|
+
description: 'Members that may join groups owned by the user, including the owner.'
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
exports.FEATURE_DEFINITIONS = [
|
|
63
|
+
{
|
|
64
|
+
key: 'cleanExport',
|
|
65
|
+
label: 'Clean export',
|
|
66
|
+
description: 'Exports and downloads without a watermark.'
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
exports.METER_KEYS = exports.METER_DEFINITIONS.map(m => m.key);
|
|
70
|
+
exports.FEATURE_KEYS = exports.FEATURE_DEFINITIONS.map(f => f.key);
|
|
71
|
+
const METER_PERIODS = Object.fromEntries(exports.METER_DEFINITIONS.map(m => [m.key, m.period]));
|
|
72
|
+
function meterPeriod(meter) {
|
|
73
|
+
return METER_PERIODS[meter] ?? 'ALL';
|
|
74
|
+
}
|
|
75
|
+
function isMeterKey(value) {
|
|
76
|
+
return exports.METER_KEYS.includes(value);
|
|
77
|
+
}
|
|
78
|
+
function isFeatureKey(value) {
|
|
79
|
+
return exports.FEATURE_KEYS.includes(value);
|
|
80
|
+
}
|
|
81
|
+
const GB = 1024 * 1024 * 1024;
|
|
82
|
+
/**
|
|
83
|
+
* Bootstrap values only — a Plan document's own `limits` wins over anything
|
|
84
|
+
* here. These are what a plan gets before an admin has ever touched it, and
|
|
85
|
+
* what the entitlement client falls back to when payment-service is down.
|
|
86
|
+
*
|
|
87
|
+
* Keep `free` conservative for exactly that reason: an outage degrades paying
|
|
88
|
+
* users to these numbers, and the alternative (fail-open) gives the product
|
|
89
|
+
* away for the length of the outage.
|
|
90
|
+
*/
|
|
91
|
+
exports.DEFAULT_PLAN_LIMITS = {
|
|
92
|
+
free: {
|
|
93
|
+
meters: { storageBytes: 1 * GB, aiJobs: 25, trackedTickers: 3, seats: 1 },
|
|
94
|
+
features: { cleanExport: false },
|
|
95
|
+
retentionDays: 90
|
|
96
|
+
},
|
|
97
|
+
personal: {
|
|
98
|
+
meters: { storageBytes: 50 * GB, aiJobs: 200, trackedTickers: 25, seats: 1 },
|
|
99
|
+
features: { cleanExport: true },
|
|
100
|
+
retentionDays: exports.UNLIMITED
|
|
101
|
+
},
|
|
102
|
+
family: {
|
|
103
|
+
meters: { storageBytes: 250 * GB, aiJobs: 600, trackedTickers: 100, seats: 5 },
|
|
104
|
+
features: { cleanExport: true },
|
|
105
|
+
retentionDays: exports.UNLIMITED
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* Everything unlimited, for the maintainers and for comped friends. Held as a
|
|
109
|
+
* real plan with a real subscription rather than an exemption branch in the
|
|
110
|
+
* code, so the people running the product go through the identical
|
|
111
|
+
* enforcement path as a paying customer — an `if (isStaff) skipEverything`
|
|
112
|
+
* flag would mean the paywall is never exercised by anyone who could notice
|
|
113
|
+
* it was broken.
|
|
114
|
+
*
|
|
115
|
+
* Kept off the public catalog through `Plan.isPublic = false`.
|
|
116
|
+
*/
|
|
117
|
+
staff: {
|
|
118
|
+
meters: { storageBytes: exports.UNLIMITED, aiJobs: exports.UNLIMITED, trackedTickers: exports.UNLIMITED, seats: exports.UNLIMITED },
|
|
119
|
+
features: { cleanExport: true },
|
|
120
|
+
retentionDays: exports.UNLIMITED
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/** Plan code the seeded staff/comp plan uses. */
|
|
124
|
+
exports.STAFF_PLAN = 'staff';
|
|
125
|
+
exports.FALLBACK_PLAN = 'free';
|
|
126
|
+
/**
|
|
127
|
+
* Code defaults for a plan code, with unknown codes resolving to free.
|
|
128
|
+
*
|
|
129
|
+
* That fallback is deliberate: a typo'd or newly-added plan code must never
|
|
130
|
+
* silently grant unlimited. Callers that want the admin-configured numbers
|
|
131
|
+
* read them from the Plan document and pass them through `normalizeLimits`;
|
|
132
|
+
* this function is the floor underneath that.
|
|
133
|
+
*/
|
|
134
|
+
function resolveLimits(planCode) {
|
|
135
|
+
const limits = exports.DEFAULT_PLAN_LIMITS[planCode ?? exports.FALLBACK_PLAN] ?? exports.DEFAULT_PLAN_LIMITS[exports.FALLBACK_PLAN];
|
|
136
|
+
return cloneLimits(limits);
|
|
137
|
+
}
|
|
138
|
+
function cloneLimits(limits) {
|
|
139
|
+
return {
|
|
140
|
+
meters: { ...limits.meters },
|
|
141
|
+
features: { ...limits.features },
|
|
142
|
+
retentionDays: limits.retentionDays
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function normalizeCeiling(value, fallback) {
|
|
146
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
147
|
+
return fallback;
|
|
148
|
+
// Anything negative means "unlimited"; nothing else is a meaningful ceiling.
|
|
149
|
+
if (value < 0)
|
|
150
|
+
return exports.UNLIMITED;
|
|
151
|
+
return Math.floor(value);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Layers a partial set of limits over a complete one.
|
|
155
|
+
*
|
|
156
|
+
* Entitlements resolve through three layers, each one partial and each one
|
|
157
|
+
* overriding the last: code defaults → the Plan document's `limits` → a
|
|
158
|
+
* per-user `EntitlementOverride`. This is the single step all three share.
|
|
159
|
+
*
|
|
160
|
+
* - unknown meter/feature keys are dropped — no store can invent enforcement;
|
|
161
|
+
* - missing keys keep the base value, so adding a new meter in code works
|
|
162
|
+
* everywhere before any stored document mentions it;
|
|
163
|
+
* - negative numbers normalize to `UNLIMITED`, fractions floor.
|
|
164
|
+
*/
|
|
165
|
+
function applyLimits(base, input) {
|
|
166
|
+
const result = cloneLimits(base);
|
|
167
|
+
if (!input)
|
|
168
|
+
return result;
|
|
169
|
+
for (const key of exports.METER_KEYS) {
|
|
170
|
+
result.meters[key] = normalizeCeiling(input.meters?.[key], result.meters[key]);
|
|
171
|
+
}
|
|
172
|
+
for (const key of exports.FEATURE_KEYS) {
|
|
173
|
+
const value = input.features?.[key];
|
|
174
|
+
result.features[key] = typeof value === 'boolean' ? value : result.features[key];
|
|
175
|
+
}
|
|
176
|
+
result.retentionDays = normalizeCeiling(input.retentionDays, result.retentionDays);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Turns whatever is stored on a Plan document (or arrived from an admin form)
|
|
181
|
+
* into a complete, valid `PlanLimits`, filling the gaps from that plan code's
|
|
182
|
+
* code defaults.
|
|
183
|
+
*/
|
|
184
|
+
function normalizeLimits(input, planCode) {
|
|
185
|
+
return applyLimits(resolveLimits(planCode), input);
|
|
186
|
+
}
|
|
187
|
+
/** True when `used + amount` fits under `limit`. `UNLIMITED` always fits. */
|
|
188
|
+
function fitsWithin(limit, used, amount = 0) {
|
|
189
|
+
if (limit === exports.UNLIMITED || limit < 0)
|
|
190
|
+
return true;
|
|
191
|
+
return used + amount <= limit;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Everything an admin UI needs to render a limits editor without hardcoding a
|
|
195
|
+
* single meter name — new keys appear in the form as soon as they exist here.
|
|
196
|
+
*/
|
|
197
|
+
exports.ENTITLEMENT_CATALOG = {
|
|
198
|
+
unlimited: exports.UNLIMITED,
|
|
199
|
+
meters: exports.METER_DEFINITIONS,
|
|
200
|
+
features: exports.FEATURE_DEFINITIONS,
|
|
201
|
+
retentionDays: {
|
|
202
|
+
key: 'retentionDays',
|
|
203
|
+
label: 'History retention (days)',
|
|
204
|
+
unit: 'days',
|
|
205
|
+
description: 'How far back reads may reach. Gates relationship history and, through the price-history range, the portfolio planner.'
|
|
206
|
+
},
|
|
207
|
+
defaults: exports.DEFAULT_PLAN_LIMITS,
|
|
208
|
+
fallbackPlan: exports.FALLBACK_PLAN
|
|
209
|
+
};
|
|
210
|
+
//# sourceMappingURL=definitions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/entitlements/definitions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { UNLIMITED, FALLBACK_PLAN, STAFF_PLAN, applyLimits, METER_KEYS, FEATURE_KEYS, METER_DEFINITIONS, FEATURE_DEFINITIONS, DEFAULT_PLAN_LIMITS, ENTITLEMENT_CATALOG, resolveLimits, normalizeLimits, meterPeriod, isMeterKey, isFeatureKey, fitsWithin } from './definitions';
|
|
2
|
+
export type { MeterKey, FeatureKey, MeterPeriod, MeterDefinition, FeatureDefinition, PlanLimits, PartialPlanLimits } from './definitions';
|
|
3
|
+
export type { Entitlements, EntitlementsMode } from './types';
|
|
4
|
+
export { getEntitlementsMode, resetEntitlementsModeWarning } from './mode';
|
|
5
|
+
export { UsageMeter, currentPeriod } from './UsageMeter';
|
|
6
|
+
export type { IUsageMeter } from './UsageMeter';
|
|
7
|
+
export { getUsage, getUsageSnapshot, checkQuota, consumeQuota, releaseQuota, setUsage } from './usage';
|
|
8
|
+
export type { QuotaRequest } from './usage';
|
|
9
|
+
export { getEntitlements, invalidateEntitlements, clearEntitlementsCache } from './client';
|
|
10
|
+
export { loadEntitlements, requireEntitlement, requireQuota, retentionFloor } from './middleware';
|
|
11
|
+
export type { EntitlementOptions, QuotaOptions, SubjectResolver } from './middleware';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/entitlements/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,UAAU,EACV,WAAW,EACX,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,WAAW,EACX,UAAU,EACV,YAAY,EACZ,UAAU,EACX,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,QAAQ,EACR,UAAU,EACV,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EAClB,MAAM,eAAe,CAAC;AAEvB,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,4BAA4B,EAAE,MAAM,QAAQ,CAAC;AAE3E,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACvG,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAE3F,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAClG,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -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"}
|