@tumbaland/backend-core 1.26.0 → 1.28.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/index.d.ts +1 -1
- package/dist/entitlements/index.d.ts.map +1 -1
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/index.js.map +1 -1
- package/dist/entitlements/middleware.d.ts +14 -3
- package/dist/entitlements/middleware.d.ts.map +1 -1
- package/dist/entitlements/middleware.js +24 -7
- package/dist/entitlements/middleware.js.map +1 -1
- package/dist/entitlements/usage.d.ts +21 -0
- package/dist/entitlements/usage.d.ts.map +1 -1
- package/dist/entitlements/usage.js +36 -0
- package/dist/entitlements/usage.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/authMiddleware.d.ts +14 -0
- package/dist/middleware/authMiddleware.d.ts.map +1 -1
- package/dist/middleware/authMiddleware.js +29 -0
- package/dist/middleware/authMiddleware.js.map +1 -1
- package/package.json +1 -1
- package/src/entitlements/index.ts +1 -1
- package/src/entitlements/middleware.test.ts +38 -0
- package/src/entitlements/middleware.ts +33 -8
- package/src/entitlements/usage.test.ts +48 -1
- package/src/entitlements/usage.ts +44 -0
- package/src/index.ts +1 -1
- package/src/middleware/authMiddleware.test.ts +70 -1
- package/src/middleware/authMiddleware.ts +29 -1
|
@@ -3,7 +3,7 @@ export type { MeterKey, FeatureKey, MeterPeriod, MeterDefinition, FeatureDefinit
|
|
|
3
3
|
export type { Entitlements } from './types';
|
|
4
4
|
export { UsageMeter, currentPeriod } from './UsageMeter';
|
|
5
5
|
export type { IUsageMeter } from './UsageMeter';
|
|
6
|
-
export { getUsage, getUsageSnapshot, checkQuota, consumeQuota, releaseQuota, setUsage } from './usage';
|
|
6
|
+
export { getUsage, getUsageSnapshot, checkQuota, consumeQuota, recordUsage, releaseQuota, setUsage } from './usage';
|
|
7
7
|
export type { QuotaRequest } from './usage';
|
|
8
8
|
export { reconcileUsageMeters, runUsageReconciliation, RECONCILED_METERS } from './reconcile';
|
|
9
9
|
export type { ReconcileOptions, ReconcileReport, MeterChange, OrphanedMeter } from './reconcile';
|
|
@@ -1 +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,MAAM,SAAS,CAAC;AAE5C,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;
|
|
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,MAAM,SAAS,CAAC;AAE5C,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,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACpH,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC9F,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjG,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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.retentionFloor = exports.requireQuota = exports.requireEntitlement = exports.loadEntitlements = exports.clearEntitlementsCache = exports.invalidateEntitlements = exports.getEntitlements = exports.RECONCILED_METERS = exports.runUsageReconciliation = exports.reconcileUsageMeters = exports.setUsage = exports.releaseQuota = exports.consumeQuota = exports.checkQuota = exports.getUsageSnapshot = exports.getUsage = exports.currentPeriod = exports.UsageMeter = 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;
|
|
3
|
+
exports.retentionFloor = exports.requireQuota = exports.requireEntitlement = exports.loadEntitlements = exports.clearEntitlementsCache = exports.invalidateEntitlements = exports.getEntitlements = exports.RECONCILED_METERS = exports.runUsageReconciliation = exports.reconcileUsageMeters = exports.setUsage = exports.releaseQuota = exports.recordUsage = exports.consumeQuota = exports.checkQuota = exports.getUsageSnapshot = exports.getUsage = exports.currentPeriod = exports.UsageMeter = 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
4
|
var definitions_1 = require("./definitions");
|
|
5
5
|
Object.defineProperty(exports, "UNLIMITED", { enumerable: true, get: function () { return definitions_1.UNLIMITED; } });
|
|
6
6
|
Object.defineProperty(exports, "FALLBACK_PLAN", { enumerable: true, get: function () { return definitions_1.FALLBACK_PLAN; } });
|
|
@@ -26,6 +26,7 @@ Object.defineProperty(exports, "getUsage", { enumerable: true, get: function ()
|
|
|
26
26
|
Object.defineProperty(exports, "getUsageSnapshot", { enumerable: true, get: function () { return usage_1.getUsageSnapshot; } });
|
|
27
27
|
Object.defineProperty(exports, "checkQuota", { enumerable: true, get: function () { return usage_1.checkQuota; } });
|
|
28
28
|
Object.defineProperty(exports, "consumeQuota", { enumerable: true, get: function () { return usage_1.consumeQuota; } });
|
|
29
|
+
Object.defineProperty(exports, "recordUsage", { enumerable: true, get: function () { return usage_1.recordUsage; } });
|
|
29
30
|
Object.defineProperty(exports, "releaseQuota", { enumerable: true, get: function () { return usage_1.releaseQuota; } });
|
|
30
31
|
Object.defineProperty(exports, "setUsage", { enumerable: true, get: function () { return usage_1.setUsage; } });
|
|
31
32
|
var reconcile_1 = require("./reconcile");
|
|
@@ -1 +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;AAcZ,2CAAyD;AAAhD,wGAAA,UAAU,OAAA;AAAE,2GAAA,aAAa,OAAA;AAElC,
|
|
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;AAcZ,2CAAyD;AAAhD,wGAAA,UAAU,OAAA;AAAE,2GAAA,aAAa,OAAA;AAElC,iCAAoH;AAA3G,iGAAA,QAAQ,OAAA;AAAE,yGAAA,gBAAgB,OAAA;AAAE,mGAAA,UAAU,OAAA;AAAE,qGAAA,YAAY,OAAA;AAAE,oGAAA,WAAW,OAAA;AAAE,qGAAA,YAAY,OAAA;AAAE,iGAAA,QAAQ,OAAA;AAGlG,yCAA8F;AAArF,iHAAA,oBAAoB,OAAA;AAAE,mHAAA,sBAAsB,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAGxE,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"}
|
|
@@ -43,12 +43,23 @@ export interface QuotaOptions extends EntitlementOptions {
|
|
|
43
43
|
*/
|
|
44
44
|
export declare function requireQuota(meter: MeterKey, options?: QuotaOptions): RequestHandler;
|
|
45
45
|
/**
|
|
46
|
-
* The oldest timestamp a
|
|
46
|
+
* The oldest timestamp a caller may read, or `null` for unlimited history.
|
|
47
47
|
*
|
|
48
|
-
* Meant to be dropped straight into
|
|
48
|
+
* Meant to be dropped straight into a `$match` or a `find` filter — this is what
|
|
49
49
|
* gates the portfolio planner, since the planner runs entirely client-side and
|
|
50
50
|
* any `<PaywallGate>` around it is bypassable from devtools. Truncating the
|
|
51
51
|
* price history it depends on is not.
|
|
52
|
+
*
|
|
53
|
+
* **Several subjects resolve to the most generous floor.** Retention is not a
|
|
54
|
+
* counter, so unlike a quota it has no single payer: data held in a group is
|
|
55
|
+
* read by every member, and if it resolved to the *caller's* plan alone, a free
|
|
56
|
+
* member of a paid Family group would see ninety days of a household's history
|
|
57
|
+
* that the household paid to keep. Pass the caller together with the owners of
|
|
58
|
+
* the groups being read, and the group's plan lifts the floor for everyone in
|
|
59
|
+
* it — which is what "shared across a household" was sold as.
|
|
60
|
+
*
|
|
61
|
+
* An empty list resolves to the free plan's floor rather than to `null`: no
|
|
62
|
+
* identifiable subject must never mean unlimited history.
|
|
52
63
|
*/
|
|
53
|
-
export declare function retentionFloor(
|
|
64
|
+
export declare function retentionFloor(subject: string | readonly string[], now?: Date): Promise<Date | null>;
|
|
54
65
|
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -1 +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,
|
|
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,EAAiB,UAAU,EAAE,QAAQ,EAA4B,MAAM,eAAe,CAAC;AAG9F,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,CAyBxG;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,CAkBxF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACnC,GAAG,GAAE,IAAiB,GACrB,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAiBtB"}
|
|
@@ -81,18 +81,35 @@ function requireQuota(meter, options = {}) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* The oldest timestamp a
|
|
84
|
+
* The oldest timestamp a caller may read, or `null` for unlimited history.
|
|
85
85
|
*
|
|
86
|
-
* Meant to be dropped straight into
|
|
86
|
+
* Meant to be dropped straight into a `$match` or a `find` filter — this is what
|
|
87
87
|
* gates the portfolio planner, since the planner runs entirely client-side and
|
|
88
88
|
* any `<PaywallGate>` around it is bypassable from devtools. Truncating the
|
|
89
89
|
* price history it depends on is not.
|
|
90
|
+
*
|
|
91
|
+
* **Several subjects resolve to the most generous floor.** Retention is not a
|
|
92
|
+
* counter, so unlike a quota it has no single payer: data held in a group is
|
|
93
|
+
* read by every member, and if it resolved to the *caller's* plan alone, a free
|
|
94
|
+
* member of a paid Family group would see ninety days of a household's history
|
|
95
|
+
* that the household paid to keep. Pass the caller together with the owners of
|
|
96
|
+
* the groups being read, and the group's plan lifts the floor for everyone in
|
|
97
|
+
* it — which is what "shared across a household" was sold as.
|
|
98
|
+
*
|
|
99
|
+
* An empty list resolves to the free plan's floor rather than to `null`: no
|
|
100
|
+
* identifiable subject must never mean unlimited history.
|
|
90
101
|
*/
|
|
91
|
-
async function retentionFloor(
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
102
|
+
async function retentionFloor(subject, now = new Date()) {
|
|
103
|
+
const emails = (Array.isArray(subject) ? subject : [subject]).filter(Boolean);
|
|
104
|
+
const floorFor = (days) => days === definitions_1.UNLIMITED || days < 0 ? null : new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
105
|
+
if (emails.length === 0) {
|
|
106
|
+
return floorFor((0, definitions_1.resolveLimits)(definitions_1.FALLBACK_PLAN).retentionDays);
|
|
107
|
+
}
|
|
108
|
+
const floors = await Promise.all(emails.map(async (email) => floorFor((await (0, client_1.getEntitlements)(email)).limits.retentionDays)));
|
|
109
|
+
// `null` is unlimited, which beats every date; otherwise the earliest floor
|
|
110
|
+
// is the one that reaches furthest back.
|
|
111
|
+
if (floors.some(floor => floor === null))
|
|
95
112
|
return null;
|
|
96
|
-
return
|
|
113
|
+
return floors.reduce((a, b) => (a < b ? a : b));
|
|
97
114
|
}
|
|
98
115
|
//# sourceMappingURL=middleware.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/entitlements/middleware.ts"],"names":[],"mappings":";;;;;AAkCA,4CASC;AAMD,gDAyBC;AAgBD,oCAkBC;
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/entitlements/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -53,4 +53,25 @@ export declare function releaseQuota(subjectId: string, meter: MeterKey, amount?
|
|
|
53
53
|
* for periodic reconciliation against the source of truth.
|
|
54
54
|
*/
|
|
55
55
|
export declare function setUsage(subjectId: string, meter: MeterKey, count: number, now?: Date): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Record usage that has already happened, without a limit check.
|
|
58
|
+
*
|
|
59
|
+
* The storage gate runs on the *signed-URL request* — the only point where a
|
|
60
|
+
* file's size is known before its bytes are written — so by the time the photo
|
|
61
|
+
* record is created the object is already in MinIO. Neither of the existing
|
|
62
|
+
* primitives fits that moment: `checkQuota` records nothing, and `consumeQuota`
|
|
63
|
+
* would reject and roll back, leaving the meter understating bytes that are
|
|
64
|
+
* genuinely stored while the user is refused a record for storage they have
|
|
65
|
+
* already paid for.
|
|
66
|
+
*
|
|
67
|
+
* So this increments past the limit deliberately. A cumulative meter can only
|
|
68
|
+
* be pushed over by a race or a stale entitlement, both bounded by one upload,
|
|
69
|
+
* and the gate then refuses the *next* one — which is the correct behaviour:
|
|
70
|
+
* the ceiling stops new writes, it does not un-store bytes.
|
|
71
|
+
*
|
|
72
|
+
* Never throws, for the same reason `releaseQuota` does not: the resource
|
|
73
|
+
* exists, the caller is on its success path, and a failed counter update is
|
|
74
|
+
* repaired by the nightly reconciliation this meter is derived for.
|
|
75
|
+
*/
|
|
76
|
+
export declare function recordUsage(subjectId: string, meter: MeterKey, amount?: number, now?: Date): Promise<void>;
|
|
56
77
|
//# sourceMappingURL=usage.d.ts.map
|
|
@@ -1 +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;AAEvC,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,4EAA4E;IAC5E,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;AAiCD;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CASjE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnE;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"}
|
|
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;AAEvC,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,4EAA4E;IAC5E,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;AAiCD;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CASjE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnE;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;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,QAAQ,EACf,MAAM,SAAI,EACV,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -9,6 +9,7 @@ exports.checkQuota = checkQuota;
|
|
|
9
9
|
exports.consumeQuota = consumeQuota;
|
|
10
10
|
exports.releaseQuota = releaseQuota;
|
|
11
11
|
exports.setUsage = setUsage;
|
|
12
|
+
exports.recordUsage = recordUsage;
|
|
12
13
|
const logger_1 = __importDefault(require("../logging/logger"));
|
|
13
14
|
const HttpError_1 = require("../errors/HttpError");
|
|
14
15
|
const UsageMeter_1 = require("./UsageMeter");
|
|
@@ -147,4 +148,39 @@ async function releaseQuota(subjectId, meter, amount = 1, now) {
|
|
|
147
148
|
async function setUsage(subjectId, meter, count, now) {
|
|
148
149
|
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 });
|
|
149
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Record usage that has already happened, without a limit check.
|
|
153
|
+
*
|
|
154
|
+
* The storage gate runs on the *signed-URL request* — the only point where a
|
|
155
|
+
* file's size is known before its bytes are written — so by the time the photo
|
|
156
|
+
* record is created the object is already in MinIO. Neither of the existing
|
|
157
|
+
* primitives fits that moment: `checkQuota` records nothing, and `consumeQuota`
|
|
158
|
+
* would reject and roll back, leaving the meter understating bytes that are
|
|
159
|
+
* genuinely stored while the user is refused a record for storage they have
|
|
160
|
+
* already paid for.
|
|
161
|
+
*
|
|
162
|
+
* So this increments past the limit deliberately. A cumulative meter can only
|
|
163
|
+
* be pushed over by a race or a stale entitlement, both bounded by one upload,
|
|
164
|
+
* and the gate then refuses the *next* one — which is the correct behaviour:
|
|
165
|
+
* the ceiling stops new writes, it does not un-store bytes.
|
|
166
|
+
*
|
|
167
|
+
* Never throws, for the same reason `releaseQuota` does not: the resource
|
|
168
|
+
* exists, the caller is on its success path, and a failed counter update is
|
|
169
|
+
* repaired by the nightly reconciliation this meter is derived for.
|
|
170
|
+
*/
|
|
171
|
+
async function recordUsage(subjectId, meter, amount = 1, now) {
|
|
172
|
+
if (amount <= 0)
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
await UsageMeter_1.UsageMeter.updateOne({ subjectId: subjectId.toLowerCase(), meter, period: (0, UsageMeter_1.currentPeriod)(meter, now) }, { $inc: { count: amount } }, { upsert: true, setDefaultsOnInsert: true });
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
logger_1.default.warn('Failed to record usage', {
|
|
179
|
+
subjectId,
|
|
180
|
+
meter,
|
|
181
|
+
amount,
|
|
182
|
+
error: err?.message
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
150
186
|
//# sourceMappingURL=usage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/entitlements/usage.ts"],"names":[],"mappings":";;;;;AAkBA,4BAOC;AAGD,4CAYC;AAyCD,gCASC;AAgBD,oCAmBC;AAUD,oCAyBC;AAOD,4BAWC;
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/entitlements/usage.ts"],"names":[],"mappings":";;;;;AAkBA,4BAOC;AAGD,4CAYC;AAyCD,gCASC;AAgBD,oCAmBC;AAUD,oCAyBC;AAOD,4BAWC;AAsBD,kCAsBC;AA9ND,+DAAuC;AACvC,mDAA2D;AAC3D,6CAAyD;AACzD,+CAA4E;AAc5E,sDAAsD;AAC/C,KAAK,UAAU,QAAQ,CAAC,SAAiB,EAAE,KAAe,EAAE,GAAU;IAC3E,MAAM,GAAG,GAAG,MAAM,uBAAU,CAAC,OAAO,CAAC;QACnC,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE;QAClC,KAAK;QACL,MAAM,EAAE,IAAA,0BAAa,EAAC,KAAK,EAAE,GAAG,CAAC;KAClC,CAAC,CAAC,IAAI,EAAE,CAAC;IACV,OAAO,GAAG,EAAE,KAAK,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,sFAAsF;AAC/E,KAAK,UAAU,gBAAgB,CAAC,SAAiB,EAAE,GAAU;IAClE,MAAM,OAAO,GAAG,wBAAU,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAA,0BAAa,EAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACxF,MAAM,IAAI,GAAG,MAAM,uBAAU,CAAC,IAAI,CAAC;QACjC,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE;QAClC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;KAC9D,CAAC,CAAC,IAAI,EAAE,CAAC;IAEV,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,wBAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAA6B,CAAC;IAC7F,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAK,wBAAgC,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,KAAiB,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC;IACzG,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,UAAU,CAAC,GAAiB,EAAE,IAAY,EAAE,KAAa;IAChE,OAAO,IAAI,gCAAoB,CAAC,qBAAqB,GAAG,CAAC,KAAK,EAAE,EAAE;QAChE,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,IAAI;QACJ,KAAK;QACL,QAAQ,EAAE,GAAG,CAAC,YAAY,CAAC,QAAQ;QACnC,SAAS,EAAE,GAAG,CAAC,YAAY,CAAC,SAAS;KACtC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,GAAiB,EAAE,IAAY,EAAE,KAAa;IAChE,gBAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE;QACjC,KAAK,EAAE,qBAAqB;QAC5B,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC;QACvB,IAAI;QACJ,KAAK;QACL,QAAQ,EAAE,GAAG,CAAC,YAAY,CAAC,QAAQ;QACnC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,KAAK,EAAE,GAAG,CAAC,YAAY,CAAC,KAAK,KAAK,IAAI;KACvC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,UAAU,CAAC,GAAiB;IAChD,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,uBAAS,CAAC;IACrE,IAAI,KAAK,KAAK,uBAAS;QAAE,OAAO;IAEhC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;IACtD,IAAI,IAAA,wBAAU,EAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;QAAE,OAAO;IAErD,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7B,MAAM,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACI,KAAK,UAAU,YAAY,CAAC,GAAiB;IAClD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,uBAAS,CAAC;IACrE,MAAM,MAAM,GAAG,IAAA,0BAAa,EAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC;IAE9C,MAAM,KAAK,GAAG,MAAM,uBAAU,CAAC,gBAAgB,CAC7C,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EACvC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAC3B,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,mBAAmB,EAAE,IAAI,EAAE,CACrE,CAAC;IAEF,IAAI,KAAK,KAAK,uBAAS,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK;QAAE,OAAO;IAExD,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;IAExC,UAAU,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;IACnC,MAAM,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACjD,MAAM,UAAU,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,YAAY,CAChC,SAAiB,EACjB,KAAe,EACf,MAAM,GAAG,CAAC,EACV,GAAU;IAEV,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,EAAE,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAA,0BAAa,EAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;QAEhG,2EAA2E;QAC3E,6EAA6E;QAC7E,MAAM,MAAM,GAAG,MAAM,uBAAU,CAAC,SAAS,CAAC,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAChH,IAAI,MAAM,CAAC,YAAY,KAAK,CAAC,EAAE,CAAC;YAC9B,sEAAsE;YACtE,yEAAyE;YACzE,MAAM,uBAAU,CAAC,SAAS,CAAC,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,gBAAM,CAAC,IAAI,CAAC,+BAA+B,EAAE;YAC3C,SAAS;YACT,KAAK;YACL,MAAM;YACN,KAAK,EAAG,GAAa,EAAE,OAAO;SAC/B,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;GAIG;AACI,KAAK,UAAU,QAAQ,CAC5B,SAAiB,EACjB,KAAe,EACf,KAAa,EACb,GAAU;IAEV,MAAM,uBAAU,CAAC,SAAS,CACxB,EAAE,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAA,0BAAa,EAAC,KAAK,EAAE,GAAG,CAAC,EAAE,EAChF,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EACnD,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAC5C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACI,KAAK,UAAU,WAAW,CAC/B,SAAiB,EACjB,KAAe,EACf,MAAM,GAAG,CAAC,EACV,GAAU;IAEV,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO;IAExB,IAAI,CAAC;QACH,MAAM,uBAAU,CAAC,SAAS,CACxB,EAAE,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAA,0BAAa,EAAC,KAAK,EAAE,GAAG,CAAC,EAAE,EAChF,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAC3B,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAC5C,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,gBAAM,CAAC,IAAI,CAAC,wBAAwB,EAAE;YACpC,SAAS;YACT,KAAK;YACL,MAAM;YACN,KAAK,EAAG,GAAa,EAAE,OAAO;SAC/B,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFound
|
|
|
12
12
|
export type { PaymentRequiredDetails } from './errors/HttpError';
|
|
13
13
|
export * from './entitlements';
|
|
14
14
|
export * from './groups';
|
|
15
|
-
export { authenticateToken } from './middleware/authMiddleware';
|
|
15
|
+
export { authenticateToken, optionalAuth } from './middleware/authMiddleware';
|
|
16
16
|
export { requireInternalServiceToken, internalServiceTokenBypass, allowUserOrInternalService, verifyInternalToken, INTERNAL_TOKEN_HEADER, SERVICE_ID_HEADER } from './middleware/internalServiceAuth';
|
|
17
17
|
export { createCorsMiddleware } from './middleware/corsMiddleware';
|
|
18
18
|
export { errorHandler } from './middleware/errorHandler';
|
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,cAAc,UAAU,CAAC;AAGzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,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,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC9E,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
|
@@ -18,8 +18,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.
|
|
22
|
-
exports.extractSpanContext = exports.injectHeaders = exports.setSpanTag = exports.logToSpan = exports.createChildSpan = exports.tracingMiddleware = exports.startSpan = exports.getTracer = void 0;
|
|
21
|
+
exports.register = exports.businessMetrics = exports.databaseQueriesTotal = exports.databaseQueryDuration = exports.httpRequestsTotal = exports.httpRequestDuration = exports.metricsMiddleware = exports.canAccessResource = exports.buildAccessQuery = exports.sendError = exports.sendSuccess = exports.createApiResponse = exports.correlationMiddleware = exports.generateCorrelationId = exports.validate = exports.strictRateLimiter = exports.standardRateLimiter = exports.createRateLimiter = exports.securityHeaders = exports.simpleRequestLogger = exports.requestLoggerWithMetrics = exports.requestLogger = exports.errorHandler = exports.createCorsMiddleware = exports.SERVICE_ID_HEADER = exports.INTERNAL_TOKEN_HEADER = exports.verifyInternalToken = exports.allowUserOrInternalService = exports.internalServiceTokenBypass = exports.requireInternalServiceToken = exports.optionalAuth = exports.authenticateToken = exports.PaymentRequiredError = exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = exports.requireEnv = exports.disconnectDB = exports.connectDB = exports.createHealthCheck = exports.metricsHandler = exports.healthLive = exports.healthCheck = exports.logger = exports.registerShutdown = exports.createBaseApp = void 0;
|
|
22
|
+
exports.extractSpanContext = exports.injectHeaders = exports.setSpanTag = exports.logToSpan = exports.createChildSpan = exports.tracingMiddleware = exports.startSpan = exports.getTracer = exports.initTracer = void 0;
|
|
23
23
|
// App bootstrap
|
|
24
24
|
var createBaseApp_1 = require("./app/createBaseApp");
|
|
25
25
|
Object.defineProperty(exports, "createBaseApp", { enumerable: true, get: function () { return createBaseApp_1.createBaseApp; } });
|
|
@@ -59,6 +59,7 @@ __exportStar(require("./groups"), exports);
|
|
|
59
59
|
// Middleware
|
|
60
60
|
var authMiddleware_1 = require("./middleware/authMiddleware");
|
|
61
61
|
Object.defineProperty(exports, "authenticateToken", { enumerable: true, get: function () { return authMiddleware_1.authenticateToken; } });
|
|
62
|
+
Object.defineProperty(exports, "optionalAuth", { enumerable: true, get: function () { return authMiddleware_1.optionalAuth; } });
|
|
62
63
|
var internalServiceAuth_1 = require("./middleware/internalServiceAuth");
|
|
63
64
|
Object.defineProperty(exports, "requireInternalServiceToken", { enumerable: true, get: function () { return internalServiceAuth_1.requireInternalServiceToken; } });
|
|
64
65
|
Object.defineProperty(exports, "internalServiceTokenBypass", { enumerable: true, get: function () { return internalServiceAuth_1.internalServiceTokenBypass; } });
|
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,0DAA0D;AAC1D,2CAAyB;AAEzB,aAAa;AACb,
|
|
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,8DAA8E;AAArE,mHAAA,iBAAiB,OAAA;AAAE,8GAAA,YAAY,OAAA;AACxC,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"}
|
|
@@ -6,4 +6,18 @@ import { Request, Response, NextFunction } from 'express';
|
|
|
6
6
|
* `../types/auth`, no `(req as any)` cast needed.
|
|
7
7
|
*/
|
|
8
8
|
export declare function authenticateToken(req: Request, res: Response, next: NextFunction): void;
|
|
9
|
+
/**
|
|
10
|
+
* Attaches the user when a valid token is present, and never rejects.
|
|
11
|
+
*
|
|
12
|
+
* For routes whose response is public but *richer* when signed in — the plan
|
|
13
|
+
* catalog is the case this exists for: a signed-out visitor on the landing
|
|
14
|
+
* page must be able to read prices, while a signed-in one additionally gets
|
|
15
|
+
* the hidden plan they are actually on.
|
|
16
|
+
*
|
|
17
|
+
* An invalid or expired token is treated as no token rather than as a 401. The
|
|
18
|
+
* caller asked for public data; refusing it because a stale cookie is lying
|
|
19
|
+
* around would break the landing page for exactly the people most likely to
|
|
20
|
+
* have one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function optionalAuth(req: Request, _res: Response, next: NextFunction): void;
|
|
9
23
|
//# sourceMappingURL=authMiddleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAiBvF"}
|
|
1
|
+
{"version":3,"file":"authMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAiBvF;AACD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAcnF"}
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.authenticateToken = authenticateToken;
|
|
7
|
+
exports.optionalAuth = optionalAuth;
|
|
7
8
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
9
|
const env_1 = require("../config/env");
|
|
9
10
|
/**
|
|
@@ -29,4 +30,32 @@ function authenticateToken(req, res, next) {
|
|
|
29
30
|
res.status(401).json({ success: false, message: 'Invalid or expired token' });
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Attaches the user when a valid token is present, and never rejects.
|
|
35
|
+
*
|
|
36
|
+
* For routes whose response is public but *richer* when signed in — the plan
|
|
37
|
+
* catalog is the case this exists for: a signed-out visitor on the landing
|
|
38
|
+
* page must be able to read prices, while a signed-in one additionally gets
|
|
39
|
+
* the hidden plan they are actually on.
|
|
40
|
+
*
|
|
41
|
+
* An invalid or expired token is treated as no token rather than as a 401. The
|
|
42
|
+
* caller asked for public data; refusing it because a stale cookie is lying
|
|
43
|
+
* around would break the landing page for exactly the people most likely to
|
|
44
|
+
* have one.
|
|
45
|
+
*/
|
|
46
|
+
function optionalAuth(req, _res, next) {
|
|
47
|
+
try {
|
|
48
|
+
const JWT_SECRET = (0, env_1.requireEnv)('JWT_SECRET');
|
|
49
|
+
const token = req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
|
|
50
|
+
if (token) {
|
|
51
|
+
const user = jsonwebtoken_1.default.verify(token, JWT_SECRET);
|
|
52
|
+
req.user = user;
|
|
53
|
+
req.userGroups = user.groups ?? [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Deliberately ignored — see above.
|
|
58
|
+
}
|
|
59
|
+
next();
|
|
60
|
+
}
|
|
32
61
|
//# sourceMappingURL=authMiddleware.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authMiddleware.js","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":";;;;;AAWA,8CAiBC;
|
|
1
|
+
{"version":3,"file":"authMiddleware.js","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":";;;;;AAWA,8CAiBC;AAcD,oCAcC;AAvDD,gEAA+B;AAC/B,uCAA2C;AAG3C;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAC/E,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAA,gBAAU,EAAC,YAAY,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE7F,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QAED,MAAM,IAAI,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAgB,CAAC;QAC1D,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAChB,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC,CAAC;IAChF,CAAC;AACH,CAAC;AACD;;;;;;;;;;;;GAYG;AACH,SAAgB,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC3E,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAA,gBAAU,EAAC,YAAY,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE7F,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAgB,CAAC;YAC1D,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;YAChB,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;QACrC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oCAAoC;IACtC,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC"}
|
package/package.json
CHANGED
|
@@ -30,7 +30,7 @@ export type { Entitlements } from './types';
|
|
|
30
30
|
|
|
31
31
|
export { UsageMeter, currentPeriod } from './UsageMeter';
|
|
32
32
|
export type { IUsageMeter } from './UsageMeter';
|
|
33
|
-
export { getUsage, getUsageSnapshot, checkQuota, consumeQuota, releaseQuota, setUsage } from './usage';
|
|
33
|
+
export { getUsage, getUsageSnapshot, checkQuota, consumeQuota, recordUsage, releaseQuota, setUsage } from './usage';
|
|
34
34
|
export type { QuotaRequest } from './usage';
|
|
35
35
|
|
|
36
36
|
export { reconcileUsageMeters, runUsageReconciliation, RECONCILED_METERS } from './reconcile';
|
|
@@ -156,3 +156,41 @@ describe('retentionFloor', () => {
|
|
|
156
156
|
expect(await retentionFloor('user@example.com', now)).toEqual(new Date('2026-05-11T00:00:00.000Z'));
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
|
+
|
|
160
|
+
describe('retentionFloor across several subjects', () => {
|
|
161
|
+
const now = new Date('2026-08-09T00:00:00.000Z');
|
|
162
|
+
|
|
163
|
+
it('takes the most generous floor, so a paid group lifts it for every member', () => {
|
|
164
|
+
mockGetEntitlements.mockImplementation(async (email: string) =>
|
|
165
|
+
email === 'owner@example.com'
|
|
166
|
+
? entitlements({ planCode: 'family', limits: normalizeLimits({ retentionDays: UNLIMITED }, 'family') })
|
|
167
|
+
: entitlements()
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// A free member reading a Family group's data must not be clipped to 90
|
|
171
|
+
// days of a household's history the household paid to keep.
|
|
172
|
+
return expect(retentionFloor(['member@example.com', 'owner@example.com'], now)).resolves.toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('picks the floor that reaches furthest back when none is unlimited', async () => {
|
|
176
|
+
mockGetEntitlements.mockImplementation(async (email: string) =>
|
|
177
|
+
entitlements({ limits: normalizeLimits({ retentionDays: email === 'owner@example.com' ? 365 : 90 }, 'free') })
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(await retentionFloor(['member@example.com', 'owner@example.com'], now)).toEqual(
|
|
181
|
+
new Date('2025-08-09T00:00:00.000Z')
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('still accepts a single email', async () => {
|
|
186
|
+
mockGetEntitlements.mockResolvedValue(entitlements());
|
|
187
|
+
|
|
188
|
+
expect(await retentionFloor('user@example.com', now)).toEqual(new Date('2026-05-11T00:00:00.000Z'));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('falls back to the free floor when no subject can be identified', async () => {
|
|
192
|
+
// An unidentifiable caller must never resolve to unlimited history.
|
|
193
|
+
expect(await retentionFloor([], now)).toEqual(new Date('2026-05-11T00:00:00.000Z'));
|
|
194
|
+
expect(mockGetEntitlements).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Request, RequestHandler } from 'express';
|
|
2
2
|
import logger from '../logging/logger';
|
|
3
3
|
import { PaymentRequiredError, UnauthorizedError } from '../errors/HttpError';
|
|
4
|
-
import { FeatureKey, MeterKey, UNLIMITED } from './definitions';
|
|
4
|
+
import { FALLBACK_PLAN, FeatureKey, MeterKey, UNLIMITED, resolveLimits } from './definitions';
|
|
5
5
|
import { getEntitlements } from './client';
|
|
6
6
|
import { checkQuota } from './usage';
|
|
7
7
|
import { Entitlements } from './types';
|
|
@@ -109,17 +109,42 @@ export function requireQuota(meter: MeterKey, options: QuotaOptions = {}): Reque
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* The oldest timestamp a
|
|
112
|
+
* The oldest timestamp a caller may read, or `null` for unlimited history.
|
|
113
113
|
*
|
|
114
|
-
* Meant to be dropped straight into
|
|
114
|
+
* Meant to be dropped straight into a `$match` or a `find` filter — this is what
|
|
115
115
|
* gates the portfolio planner, since the planner runs entirely client-side and
|
|
116
116
|
* any `<PaywallGate>` around it is bypassable from devtools. Truncating the
|
|
117
117
|
* price history it depends on is not.
|
|
118
|
+
*
|
|
119
|
+
* **Several subjects resolve to the most generous floor.** Retention is not a
|
|
120
|
+
* counter, so unlike a quota it has no single payer: data held in a group is
|
|
121
|
+
* read by every member, and if it resolved to the *caller's* plan alone, a free
|
|
122
|
+
* member of a paid Family group would see ninety days of a household's history
|
|
123
|
+
* that the household paid to keep. Pass the caller together with the owners of
|
|
124
|
+
* the groups being read, and the group's plan lifts the floor for everyone in
|
|
125
|
+
* it — which is what "shared across a household" was sold as.
|
|
126
|
+
*
|
|
127
|
+
* An empty list resolves to the free plan's floor rather than to `null`: no
|
|
128
|
+
* identifiable subject must never mean unlimited history.
|
|
118
129
|
*/
|
|
119
|
-
export async function retentionFloor(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
export async function retentionFloor(
|
|
131
|
+
subject: string | readonly string[],
|
|
132
|
+
now: Date = new Date()
|
|
133
|
+
): Promise<Date | null> {
|
|
134
|
+
const emails = (Array.isArray(subject) ? subject : [subject as string]).filter(Boolean);
|
|
135
|
+
const floorFor = (days: number): Date | null =>
|
|
136
|
+
days === UNLIMITED || days < 0 ? null : new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
137
|
+
|
|
138
|
+
if (emails.length === 0) {
|
|
139
|
+
return floorFor(resolveLimits(FALLBACK_PLAN).retentionDays);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const floors = await Promise.all(
|
|
143
|
+
emails.map(async email => floorFor((await getEntitlements(email)).limits.retentionDays))
|
|
144
|
+
);
|
|
123
145
|
|
|
124
|
-
|
|
146
|
+
// `null` is unlimited, which beats every date; otherwise the earliest floor
|
|
147
|
+
// is the one that reaches furthest back.
|
|
148
|
+
if (floors.some(floor => floor === null)) return null;
|
|
149
|
+
return (floors as Date[]).reduce((a, b) => (a < b ? a : b));
|
|
125
150
|
}
|
|
@@ -18,7 +18,7 @@ jest.mock('../logging/logger', () => ({
|
|
|
18
18
|
|
|
19
19
|
import logger from '../logging/logger';
|
|
20
20
|
import { UsageMeter, currentPeriod } from './UsageMeter';
|
|
21
|
-
import { checkQuota, consumeQuota, getUsage, getUsageSnapshot, releaseQuota, setUsage } from './usage';
|
|
21
|
+
import { checkQuota, consumeQuota, getUsage, getUsageSnapshot, recordUsage, releaseQuota, setUsage } from './usage';
|
|
22
22
|
import { Entitlements } from './types';
|
|
23
23
|
import { UNLIMITED, normalizeLimits } from './definitions';
|
|
24
24
|
|
|
@@ -241,6 +241,53 @@ describe('releaseQuota', () => {
|
|
|
241
241
|
});
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
describe('recordUsage', () => {
|
|
245
|
+
it('increments a meter that does not exist yet', async () => {
|
|
246
|
+
await recordUsage('user@example.com', 'storageBytes', 4_096);
|
|
247
|
+
|
|
248
|
+
expect(await getUsage('user@example.com', 'storageBytes')).toBe(4_096);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('records past the limit rather than refusing bytes already on disk', async () => {
|
|
252
|
+
// The gate ran at the signed-URL request; these bytes are in MinIO whatever
|
|
253
|
+
// the counter says. Rolling the increment back — what consumeQuota would do
|
|
254
|
+
// — would understate real storage and let the next upload through too.
|
|
255
|
+
await setUsage('user@example.com', 'storageBytes', 900);
|
|
256
|
+
|
|
257
|
+
await recordUsage('user@example.com', 'storageBytes', 500);
|
|
258
|
+
|
|
259
|
+
expect(await getUsage('user@example.com', 'storageBytes')).toBe(1_400);
|
|
260
|
+
// And the next request is the one that is refused.
|
|
261
|
+
await expect(
|
|
262
|
+
checkQuota({ subjectId: 'user@example.com', meter: 'storageBytes', amount: 1, entitlements: entitlements() })
|
|
263
|
+
).rejects.toMatchObject({ statusCode: 402 });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('does not lose concurrent increments', async () => {
|
|
267
|
+
await Promise.all(Array.from({ length: 10 }, () => recordUsage('user@example.com', 'storageBytes', 10)));
|
|
268
|
+
|
|
269
|
+
expect(await getUsage('user@example.com', 'storageBytes')).toBe(100);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('ignores a non-positive amount instead of writing a no-op row', async () => {
|
|
273
|
+
await recordUsage('user@example.com', 'storageBytes', 0);
|
|
274
|
+
await recordUsage('user@example.com', 'storageBytes', -50);
|
|
275
|
+
|
|
276
|
+
expect(await UsageMeter.countDocuments({ subjectId: 'user@example.com' })).toBe(0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('never throws — the resource exists and the caller is on its success path', async () => {
|
|
280
|
+
const failing = jest
|
|
281
|
+
.spyOn(UsageMeter, 'updateOne')
|
|
282
|
+
.mockRejectedValueOnce(new Error('connection lost') as never);
|
|
283
|
+
|
|
284
|
+
await expect(recordUsage('user@example.com', 'storageBytes', 10)).resolves.toBeUndefined();
|
|
285
|
+
expect(logger.warn).toHaveBeenCalledWith('Failed to record usage', expect.objectContaining({ meter: 'storageBytes' }));
|
|
286
|
+
|
|
287
|
+
failing.mockRestore();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
244
291
|
describe('getUsageSnapshot', () => {
|
|
245
292
|
it('reports every meter, defaulting the untouched ones to zero', async () => {
|
|
246
293
|
await setUsage('user@example.com', 'storageBytes', 42);
|
|
@@ -177,3 +177,47 @@ export async function setUsage(
|
|
|
177
177
|
{ upsert: true, setDefaultsOnInsert: true }
|
|
178
178
|
);
|
|
179
179
|
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Record usage that has already happened, without a limit check.
|
|
183
|
+
*
|
|
184
|
+
* The storage gate runs on the *signed-URL request* — the only point where a
|
|
185
|
+
* file's size is known before its bytes are written — so by the time the photo
|
|
186
|
+
* record is created the object is already in MinIO. Neither of the existing
|
|
187
|
+
* primitives fits that moment: `checkQuota` records nothing, and `consumeQuota`
|
|
188
|
+
* would reject and roll back, leaving the meter understating bytes that are
|
|
189
|
+
* genuinely stored while the user is refused a record for storage they have
|
|
190
|
+
* already paid for.
|
|
191
|
+
*
|
|
192
|
+
* So this increments past the limit deliberately. A cumulative meter can only
|
|
193
|
+
* be pushed over by a race or a stale entitlement, both bounded by one upload,
|
|
194
|
+
* and the gate then refuses the *next* one — which is the correct behaviour:
|
|
195
|
+
* the ceiling stops new writes, it does not un-store bytes.
|
|
196
|
+
*
|
|
197
|
+
* Never throws, for the same reason `releaseQuota` does not: the resource
|
|
198
|
+
* exists, the caller is on its success path, and a failed counter update is
|
|
199
|
+
* repaired by the nightly reconciliation this meter is derived for.
|
|
200
|
+
*/
|
|
201
|
+
export async function recordUsage(
|
|
202
|
+
subjectId: string,
|
|
203
|
+
meter: MeterKey,
|
|
204
|
+
amount = 1,
|
|
205
|
+
now?: Date
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
if (amount <= 0) return;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
await UsageMeter.updateOne(
|
|
211
|
+
{ subjectId: subjectId.toLowerCase(), meter, period: currentPeriod(meter, now) },
|
|
212
|
+
{ $inc: { count: amount } },
|
|
213
|
+
{ upsert: true, setDefaultsOnInsert: true }
|
|
214
|
+
);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
logger.warn('Failed to record usage', {
|
|
217
|
+
subjectId,
|
|
218
|
+
meter,
|
|
219
|
+
amount,
|
|
220
|
+
error: (err as Error)?.message
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ export * from './entitlements';
|
|
|
31
31
|
export * from './groups';
|
|
32
32
|
|
|
33
33
|
// Middleware
|
|
34
|
-
export { authenticateToken } from './middleware/authMiddleware';
|
|
34
|
+
export { authenticateToken, optionalAuth } from './middleware/authMiddleware';
|
|
35
35
|
export {
|
|
36
36
|
requireInternalServiceToken,
|
|
37
37
|
internalServiceTokenBypass,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Request, Response } from 'express';
|
|
2
2
|
import jwt from 'jsonwebtoken';
|
|
3
|
-
import { authenticateToken } from './authMiddleware';
|
|
3
|
+
import { authenticateToken, optionalAuth } from './authMiddleware';
|
|
4
4
|
import { UserPayload } from '../types/auth';
|
|
5
5
|
|
|
6
6
|
function mockReq(overrides: Partial<Request> = {}): Request {
|
|
@@ -102,3 +102,72 @@ describe('authenticateToken', () => {
|
|
|
102
102
|
expect(res.status).toHaveBeenCalledWith(401);
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
describe('optionalAuth', () => {
|
|
107
|
+
const ORIGINAL_ENV = process.env;
|
|
108
|
+
const next = jest.fn();
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
process.env = { ...ORIGINAL_ENV, JWT_SECRET: 'test-secret' };
|
|
112
|
+
next.mockClear();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
afterAll(() => {
|
|
116
|
+
process.env = ORIGINAL_ENV;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const payload: UserPayload = { id: '1', email: 'user@example.com', name: 'Test User' };
|
|
120
|
+
|
|
121
|
+
it('attaches the user when the token is valid', () => {
|
|
122
|
+
const req = mockReq({ cookies: { access_token: jwt.sign(payload, 'test-secret') } });
|
|
123
|
+
|
|
124
|
+
optionalAuth(req, mockRes(), next);
|
|
125
|
+
|
|
126
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
127
|
+
expect(req.user).toMatchObject({ email: 'user@example.com' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('reads a bearer token too', () => {
|
|
131
|
+
const req = mockReq({ headers: { authorization: `Bearer ${jwt.sign(payload, 'test-secret')}` } });
|
|
132
|
+
|
|
133
|
+
optionalAuth(req, mockRes(), next);
|
|
134
|
+
|
|
135
|
+
expect(req.user).toMatchObject({ email: 'user@example.com' });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('continues anonymously when there is no token', () => {
|
|
139
|
+
const req = mockReq();
|
|
140
|
+
const res = mockRes();
|
|
141
|
+
|
|
142
|
+
optionalAuth(req, res, next);
|
|
143
|
+
|
|
144
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(req.user).toBeUndefined();
|
|
146
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('treats an expired or forged token as no token rather than as a 401', () => {
|
|
150
|
+
// The caller asked for public data. Refusing it because a stale cookie is
|
|
151
|
+
// lying around would break the landing page for exactly the people most
|
|
152
|
+
// likely to have one.
|
|
153
|
+
const req = mockReq({ cookies: { access_token: jwt.sign(payload, 'wrong-secret') } });
|
|
154
|
+
const res = mockRes();
|
|
155
|
+
|
|
156
|
+
optionalAuth(req, res, next);
|
|
157
|
+
|
|
158
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(req.user).toBeUndefined();
|
|
160
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('continues anonymously when JWT_SECRET is not configured', () => {
|
|
164
|
+
delete process.env.JWT_SECRET;
|
|
165
|
+
const req = mockReq({ cookies: { access_token: 'anything' } });
|
|
166
|
+
const res = mockRes();
|
|
167
|
+
|
|
168
|
+
optionalAuth(req, res, next);
|
|
169
|
+
|
|
170
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
171
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -26,4 +26,32 @@ export function authenticateToken(req: Request, res: Response, next: NextFunctio
|
|
|
26
26
|
} catch (error) {
|
|
27
27
|
res.status(401).json({ success: false, message: 'Invalid or expired token' });
|
|
28
28
|
}
|
|
29
|
-
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Attaches the user when a valid token is present, and never rejects.
|
|
32
|
+
*
|
|
33
|
+
* For routes whose response is public but *richer* when signed in — the plan
|
|
34
|
+
* catalog is the case this exists for: a signed-out visitor on the landing
|
|
35
|
+
* page must be able to read prices, while a signed-in one additionally gets
|
|
36
|
+
* the hidden plan they are actually on.
|
|
37
|
+
*
|
|
38
|
+
* An invalid or expired token is treated as no token rather than as a 401. The
|
|
39
|
+
* caller asked for public data; refusing it because a stale cookie is lying
|
|
40
|
+
* around would break the landing page for exactly the people most likely to
|
|
41
|
+
* have one.
|
|
42
|
+
*/
|
|
43
|
+
export function optionalAuth(req: Request, _res: Response, next: NextFunction): void {
|
|
44
|
+
try {
|
|
45
|
+
const JWT_SECRET = requireEnv('JWT_SECRET');
|
|
46
|
+
const token = req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
|
|
47
|
+
|
|
48
|
+
if (token) {
|
|
49
|
+
const user = jwt.verify(token, JWT_SECRET) as UserPayload;
|
|
50
|
+
req.user = user;
|
|
51
|
+
req.userGroups = user.groups ?? [];
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Deliberately ignored — see above.
|
|
55
|
+
}
|
|
56
|
+
next();
|
|
57
|
+
}
|