@tumbaland/backend-core 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entitlements/client.d.ts.map +1 -1
- package/dist/entitlements/client.js +17 -1
- package/dist/entitlements/client.js.map +1 -1
- package/dist/entitlements/index.d.ts +3 -2
- package/dist/entitlements/index.d.ts.map +1 -1
- package/dist/entitlements/index.js +5 -4
- package/dist/entitlements/index.js.map +1 -1
- package/dist/entitlements/middleware.d.ts.map +1 -1
- package/dist/entitlements/middleware.js +8 -19
- package/dist/entitlements/middleware.js.map +1 -1
- package/dist/entitlements/reconcile.d.ts +74 -0
- package/dist/entitlements/reconcile.d.ts.map +1 -0
- package/dist/entitlements/reconcile.js +297 -0
- package/dist/entitlements/reconcile.js.map +1 -0
- package/dist/entitlements/types.d.ts +0 -8
- package/dist/entitlements/types.d.ts.map +1 -1
- package/dist/entitlements/usage.d.ts +1 -1
- package/dist/entitlements/usage.d.ts.map +1 -1
- package/dist/entitlements/usage.js +11 -20
- package/dist/entitlements/usage.js.map +1 -1
- package/dist/groups/client.d.ts +27 -0
- package/dist/groups/client.d.ts.map +1 -0
- package/dist/groups/client.js +153 -0
- package/dist/groups/client.js.map +1 -0
- package/dist/groups/index.d.ts +5 -0
- package/dist/groups/index.d.ts.map +1 -0
- package/dist/groups/index.js +10 -0
- package/dist/groups/index.js.map +1 -0
- package/dist/groups/subject.d.ts +30 -0
- package/dist/groups/subject.d.ts.map +1 -0
- package/dist/groups/subject.js +52 -0
- package/dist/groups/subject.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/entitlements/client.test.ts +30 -0
- package/src/entitlements/client.ts +18 -1
- package/src/entitlements/index.ts +4 -2
- package/src/entitlements/middleware.test.ts +9 -30
- package/src/entitlements/middleware.ts +8 -19
- package/src/entitlements/reconcile.test.ts +333 -0
- package/src/entitlements/reconcile.ts +384 -0
- package/src/entitlements/types.ts +0 -9
- package/src/entitlements/usage.test.ts +27 -31
- package/src/entitlements/usage.ts +12 -22
- package/src/groups/client.test.ts +215 -0
- package/src/groups/client.ts +182 -0
- package/src/groups/index.ts +5 -0
- package/src/groups/subject.test.ts +85 -0
- package/src/groups/subject.ts +50 -0
- package/src/index.ts +3 -0
- package/dist/entitlements/mode.d.ts +0 -10
- package/dist/entitlements/mode.d.ts.map +0 -1
- package/dist/entitlements/mode.js +0 -32
- package/dist/entitlements/mode.js.map +0 -1
- package/src/entitlements/mode.test.ts +0 -50
- package/src/entitlements/mode.ts +0 -28
|
@@ -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;
|
|
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"}
|
|
@@ -13,7 +13,6 @@ const logger_1 = __importDefault(require("../logging/logger"));
|
|
|
13
13
|
const HttpError_1 = require("../errors/HttpError");
|
|
14
14
|
const UsageMeter_1 = require("./UsageMeter");
|
|
15
15
|
const definitions_1 = require("./definitions");
|
|
16
|
-
const mode_1 = require("./mode");
|
|
17
16
|
/** Current count for one meter in its live period. */
|
|
18
17
|
async function getUsage(subjectId, meter, now) {
|
|
19
18
|
const doc = await UsageMeter_1.UsageMeter.findOne({
|
|
@@ -47,9 +46,15 @@ function quotaError(req, used, limit) {
|
|
|
47
46
|
upgradeTo: req.entitlements.upgradeTo
|
|
48
47
|
});
|
|
49
48
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Every rejection is logged, not just thrown. The 402 reaches the user, but
|
|
51
|
+
* only this line reaches the operator — and the two questions worth asking of a
|
|
52
|
+
* paywall (which meter fires most, and how often it fires on a *stale* fallback
|
|
53
|
+
* rather than on real limits) are answerable from nothing else.
|
|
54
|
+
*/
|
|
55
|
+
function logBlocked(req, used, limit) {
|
|
56
|
+
logger_1.default.info('entitlement.blocked', {
|
|
57
|
+
event: 'entitlement.blocked',
|
|
53
58
|
meter: req.meter,
|
|
54
59
|
subjectId: req.subjectId,
|
|
55
60
|
amount: req.amount ?? 1,
|
|
@@ -69,19 +74,13 @@ function logWouldBlock(req, used, limit) {
|
|
|
69
74
|
* the underlying data. Use `consumeQuota` where the count is the only record.
|
|
70
75
|
*/
|
|
71
76
|
async function checkQuota(req) {
|
|
72
|
-
const mode = (0, mode_1.getEntitlementsMode)();
|
|
73
|
-
if (mode === 'off')
|
|
74
|
-
return;
|
|
75
77
|
const limit = req.entitlements.limits.meters[req.meter] ?? definitions_1.UNLIMITED;
|
|
76
78
|
if (limit === definitions_1.UNLIMITED)
|
|
77
79
|
return;
|
|
78
80
|
const used = await getUsage(req.subjectId, req.meter);
|
|
79
81
|
if ((0, definitions_1.fitsWithin)(limit, used, req.amount ?? 1))
|
|
80
82
|
return;
|
|
81
|
-
|
|
82
|
-
logWouldBlock(req, used, limit);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
83
|
+
logBlocked(req, used, limit);
|
|
85
84
|
throw quotaError(req, used, limit);
|
|
86
85
|
}
|
|
87
86
|
/**
|
|
@@ -99,9 +98,6 @@ async function checkQuota(req) {
|
|
|
99
98
|
* who cannot upload despite being well under their limit.
|
|
100
99
|
*/
|
|
101
100
|
async function consumeQuota(req) {
|
|
102
|
-
const mode = (0, mode_1.getEntitlementsMode)();
|
|
103
|
-
if (mode === 'off')
|
|
104
|
-
return;
|
|
105
101
|
const amount = req.amount ?? 1;
|
|
106
102
|
const limit = req.entitlements.limits.meters[req.meter] ?? definitions_1.UNLIMITED;
|
|
107
103
|
const period = (0, UsageMeter_1.currentPeriod)(req.meter);
|
|
@@ -110,12 +106,7 @@ async function consumeQuota(req) {
|
|
|
110
106
|
if (limit === definitions_1.UNLIMITED || usage.count <= limit)
|
|
111
107
|
return;
|
|
112
108
|
const usedBefore = usage.count - amount;
|
|
113
|
-
|
|
114
|
-
// Keep the increment: observe mode exists to measure real demand, and a
|
|
115
|
-
// rolled-back count would under-report exactly the users who matter.
|
|
116
|
-
logWouldBlock(req, usedBefore, limit);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
109
|
+
logBlocked(req, usedBefore, limit);
|
|
119
110
|
await releaseQuota(subjectId, req.meter, amount);
|
|
120
111
|
throw quotaError(req, usedBefore, limit);
|
|
121
112
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/entitlements/usage.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/entitlements/usage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface GroupOwner {
|
|
2
|
+
groupId: string;
|
|
3
|
+
ownerEmail: string;
|
|
4
|
+
memberCount: number;
|
|
5
|
+
/** Soft-deleted groups still resolve: their photos still occupy storage. */
|
|
6
|
+
deleted: boolean;
|
|
7
|
+
/** True when group-service could not be reached and nothing was resolved. */
|
|
8
|
+
stale?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The group's owner, cached for {@link CACHE_TTL_MS}, or `null` when the group
|
|
12
|
+
* does not exist or group-service could not answer.
|
|
13
|
+
*
|
|
14
|
+
* Nothing negative is cached — neither a missing group nor a failure. A group
|
|
15
|
+
* that 404s is nearly always a caller passing a bad id, so caching it would
|
|
16
|
+
* spend memory on garbage, and caching a failure would keep billing the wrong
|
|
17
|
+
* subject for a minute after the outage ended.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getGroupOwner(groupId: string): Promise<GroupOwner | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Drop a group's cached owner. Call after anything that moves the `owner` role,
|
|
22
|
+
* to collapse the window in which usage bills to the previous owner.
|
|
23
|
+
*/
|
|
24
|
+
export declare function invalidateGroupOwner(groupId: string): void;
|
|
25
|
+
/** Empties the whole cache. Test seam, and a lever after a bulk ownership edit. */
|
|
26
|
+
export declare function clearGroupOwnerCache(): void;
|
|
27
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/groups/client.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAyGD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqB/E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1D;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
|
|
@@ -0,0 +1,153 @@
|
|
|
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.getGroupOwner = getGroupOwner;
|
|
7
|
+
exports.invalidateGroupOwner = invalidateGroupOwner;
|
|
8
|
+
exports.clearGroupOwnerCache = clearGroupOwnerCache;
|
|
9
|
+
const logger_1 = __importDefault(require("../logging/logger"));
|
|
10
|
+
/**
|
|
11
|
+
* Who a group's pooled usage bills to, read over the internal-service channel.
|
|
12
|
+
*
|
|
13
|
+
* The JWT carries `groups` — the ids a user belongs to — but never says who
|
|
14
|
+
* owns them, and no service outside group-service has the collection. So a
|
|
15
|
+
* chokepoint that needs the *quota subject* for a group album or a group ticker
|
|
16
|
+
* has to ask. See MONETIZATION.md's note on why that subject is the owner and
|
|
17
|
+
* not the uploader.
|
|
18
|
+
*
|
|
19
|
+
* Same 60s TTL as the entitlement client, for a weaker reason: ownership moves
|
|
20
|
+
* far more rarely than a plan does, so the cache could be much longer, but a
|
|
21
|
+
* stale owner bills the wrong person and one shared number is one fewer thing
|
|
22
|
+
* to reason about. The enforcement points are writes, so the traffic is low
|
|
23
|
+
* either way.
|
|
24
|
+
*/
|
|
25
|
+
const CACHE_TTL_MS = 60_000;
|
|
26
|
+
/** Bounded so a service under enumeration can't grow the cache without limit. */
|
|
27
|
+
const MAX_CACHE_ENTRIES = 5_000;
|
|
28
|
+
const REQUEST_TIMEOUT_MS = 3_000;
|
|
29
|
+
const cache = new Map();
|
|
30
|
+
/**
|
|
31
|
+
* Concurrent callers for one group share a request — a burst of uploads into
|
|
32
|
+
* the same shared album is the expected shape of this traffic.
|
|
33
|
+
*/
|
|
34
|
+
const inFlight = new Map();
|
|
35
|
+
function putInCache(groupId, value) {
|
|
36
|
+
if (cache.size >= MAX_CACHE_ENTRIES) {
|
|
37
|
+
// Map iterates in insertion order, so the first key is the oldest write.
|
|
38
|
+
const oldest = cache.keys().next();
|
|
39
|
+
if (!oldest.done)
|
|
40
|
+
cache.delete(oldest.value);
|
|
41
|
+
}
|
|
42
|
+
cache.set(groupId, { value, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Prefers the container-network address over the public hostname.
|
|
46
|
+
*
|
|
47
|
+
* `GROUP_API_URL` cannot simply be pointed inward: the frontend containers
|
|
48
|
+
* template that same variable into the `config.json` the *browser* downloads,
|
|
49
|
+
* so a Docker-internal value there breaks every group call in the UI. One
|
|
50
|
+
* variable genuinely has two consumers with incompatible needs, so this is the
|
|
51
|
+
* server-side half of it.
|
|
52
|
+
*
|
|
53
|
+
* Falling back to the public URL keeps local dev (where only `GROUP_API_URL` is
|
|
54
|
+
* set) and any un-migrated deployment working — it just takes the long way
|
|
55
|
+
* round, out through the proxy and back, carrying the internal token across the
|
|
56
|
+
* public edge.
|
|
57
|
+
*/
|
|
58
|
+
function internalGroupUrl() {
|
|
59
|
+
return process.env.GROUP_INTERNAL_URL || process.env.GROUP_API_URL;
|
|
60
|
+
}
|
|
61
|
+
async function fetchGroupOwner(groupId) {
|
|
62
|
+
const baseUrl = internalGroupUrl();
|
|
63
|
+
const token = process.env.INTERNAL_SERVICE_TOKEN;
|
|
64
|
+
const serviceId = process.env.SERVICE_NAME || 'unknown-service';
|
|
65
|
+
if (!baseUrl || !token) {
|
|
66
|
+
logger_1.default.error('Group owner lookup not configured', {
|
|
67
|
+
hasGroupApiUrl: Boolean(baseUrl),
|
|
68
|
+
hasInternalToken: Boolean(token)
|
|
69
|
+
});
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(`${baseUrl}/internal/groups/${encodeURIComponent(groupId)}/owner`, {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'x-internal-token': token,
|
|
78
|
+
'x-service-id': serviceId
|
|
79
|
+
},
|
|
80
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
81
|
+
});
|
|
82
|
+
// A 404 is an answer, not an outage: the group is gone. Distinguished from
|
|
83
|
+
// an unreachable service because the two want opposite handling — one is
|
|
84
|
+
// permanent and cacheable, the other must be retried on the next request.
|
|
85
|
+
if (response.status === 404) {
|
|
86
|
+
logger_1.default.warn('Group owner lookup found no such group', { groupId });
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
logger_1.default.error('Group owner lookup failed', { groupId, status: response.status });
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const body = (await response.json());
|
|
94
|
+
if (!body.ownerEmail) {
|
|
95
|
+
logger_1.default.error('Group owner lookup returned no owner', { groupId });
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
groupId: body.groupId ?? groupId,
|
|
100
|
+
ownerEmail: body.ownerEmail.trim().toLowerCase(),
|
|
101
|
+
memberCount: body.memberCount ?? 0,
|
|
102
|
+
deleted: body.deleted === true
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
logger_1.default.error('Group owner lookup errored', { groupId, error: err?.message });
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The group's owner, cached for {@link CACHE_TTL_MS}, or `null` when the group
|
|
112
|
+
* does not exist or group-service could not answer.
|
|
113
|
+
*
|
|
114
|
+
* Nothing negative is cached — neither a missing group nor a failure. A group
|
|
115
|
+
* that 404s is nearly always a caller passing a bad id, so caching it would
|
|
116
|
+
* spend memory on garbage, and caching a failure would keep billing the wrong
|
|
117
|
+
* subject for a minute after the outage ended.
|
|
118
|
+
*/
|
|
119
|
+
async function getGroupOwner(groupId) {
|
|
120
|
+
const key = String(groupId).trim();
|
|
121
|
+
if (!key)
|
|
122
|
+
return null;
|
|
123
|
+
const cached = cache.get(key);
|
|
124
|
+
if (cached && cached.expiresAt > Date.now())
|
|
125
|
+
return cached.value;
|
|
126
|
+
const pending = inFlight.get(key);
|
|
127
|
+
if (pending)
|
|
128
|
+
return pending;
|
|
129
|
+
const request = fetchGroupOwner(key)
|
|
130
|
+
.then(value => {
|
|
131
|
+
if (value)
|
|
132
|
+
putInCache(key, value);
|
|
133
|
+
return value;
|
|
134
|
+
})
|
|
135
|
+
.finally(() => {
|
|
136
|
+
inFlight.delete(key);
|
|
137
|
+
});
|
|
138
|
+
inFlight.set(key, request);
|
|
139
|
+
return request;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Drop a group's cached owner. Call after anything that moves the `owner` role,
|
|
143
|
+
* to collapse the window in which usage bills to the previous owner.
|
|
144
|
+
*/
|
|
145
|
+
function invalidateGroupOwner(groupId) {
|
|
146
|
+
cache.delete(String(groupId).trim());
|
|
147
|
+
}
|
|
148
|
+
/** Empties the whole cache. Test seam, and a lever after a bulk ownership edit. */
|
|
149
|
+
function clearGroupOwnerCache() {
|
|
150
|
+
cache.clear();
|
|
151
|
+
inFlight.clear();
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/groups/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/groups/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.groupOwnerSubject = exports.clearGroupOwnerCache = exports.invalidateGroupOwner = exports.getGroupOwner = void 0;
|
|
4
|
+
var client_1 = require("./client");
|
|
5
|
+
Object.defineProperty(exports, "getGroupOwner", { enumerable: true, get: function () { return client_1.getGroupOwner; } });
|
|
6
|
+
Object.defineProperty(exports, "invalidateGroupOwner", { enumerable: true, get: function () { return client_1.invalidateGroupOwner; } });
|
|
7
|
+
Object.defineProperty(exports, "clearGroupOwnerCache", { enumerable: true, get: function () { return client_1.clearGroupOwnerCache; } });
|
|
8
|
+
var subject_1 = require("./subject");
|
|
9
|
+
Object.defineProperty(exports, "groupOwnerSubject", { enumerable: true, get: function () { return subject_1.groupOwnerSubject; } });
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/groups/index.ts"],"names":[],"mappings":";;;AAAA,mCAAqF;AAA5E,uGAAA,aAAa,OAAA;AAAE,8GAAA,oBAAoB,OAAA;AAAE,8GAAA,oBAAoB,OAAA;AAGlE,qCAA8C;AAArC,4GAAA,iBAAiB,OAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Request } from 'express';
|
|
2
|
+
import { SubjectResolver } from '../entitlements/middleware';
|
|
3
|
+
export type GroupIdResolver = (req: Request) => string | undefined | Promise<string | undefined>;
|
|
4
|
+
/**
|
|
5
|
+
* Builds the `subject` resolver that `requireQuota` / `requireEntitlement`
|
|
6
|
+
* already accept, for the meters whose resource can live in a group.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* requireQuota('storageBytes', {
|
|
10
|
+
* subject: groupOwnerSubject(req => req.body.groupId),
|
|
11
|
+
* amount: req => req.body.sizeBytes
|
|
12
|
+
* })
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Personal rows keep billing to the caller; group rows bill to the group's
|
|
16
|
+
* owner, because Family sells *pooled* storage and seats and the uploader's own
|
|
17
|
+
* free tier is not the pool.
|
|
18
|
+
*
|
|
19
|
+
* **When the owner cannot be resolved, it bills the caller and says so.** The
|
|
20
|
+
* alternative — refusing the write — is harsher than what the entitlement
|
|
21
|
+
* client itself does for the same class of outage (it degrades to free limits
|
|
22
|
+
* rather than denying), and the error direction here is bounded in the shape
|
|
23
|
+
* that matters: the common case is a member on a cheap personal plan uploading
|
|
24
|
+
* into a group owned by someone on Family, so falling back to the caller checks
|
|
25
|
+
* against the *smaller* allowance. It can over-grant when the caller happens to
|
|
26
|
+
* hold the richer plan, which is why it logs at `error` — a run of these means
|
|
27
|
+
* group-service is down, not that a limit is wrong.
|
|
28
|
+
*/
|
|
29
|
+
export declare function groupOwnerSubject(resolveGroupId: GroupIdResolver): SubjectResolver;
|
|
30
|
+
//# sourceMappingURL=subject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subject.d.ts","sourceRoot":"","sources":["../../src/groups/subject.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAG7D,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,CAAC,cAAc,EAAE,eAAe,GAAG,eAAe,CAiBlF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.groupOwnerSubject = groupOwnerSubject;
|
|
7
|
+
const logger_1 = __importDefault(require("../logging/logger"));
|
|
8
|
+
const client_1 = require("./client");
|
|
9
|
+
/**
|
|
10
|
+
* Builds the `subject` resolver that `requireQuota` / `requireEntitlement`
|
|
11
|
+
* already accept, for the meters whose resource can live in a group.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* requireQuota('storageBytes', {
|
|
15
|
+
* subject: groupOwnerSubject(req => req.body.groupId),
|
|
16
|
+
* amount: req => req.body.sizeBytes
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Personal rows keep billing to the caller; group rows bill to the group's
|
|
21
|
+
* owner, because Family sells *pooled* storage and seats and the uploader's own
|
|
22
|
+
* free tier is not the pool.
|
|
23
|
+
*
|
|
24
|
+
* **When the owner cannot be resolved, it bills the caller and says so.** The
|
|
25
|
+
* alternative — refusing the write — is harsher than what the entitlement
|
|
26
|
+
* client itself does for the same class of outage (it degrades to free limits
|
|
27
|
+
* rather than denying), and the error direction here is bounded in the shape
|
|
28
|
+
* that matters: the common case is a member on a cheap personal plan uploading
|
|
29
|
+
* into a group owned by someone on Family, so falling back to the caller checks
|
|
30
|
+
* against the *smaller* allowance. It can over-grant when the caller happens to
|
|
31
|
+
* hold the richer plan, which is why it logs at `error` — a run of these means
|
|
32
|
+
* group-service is down, not that a limit is wrong.
|
|
33
|
+
*/
|
|
34
|
+
function groupOwnerSubject(resolveGroupId) {
|
|
35
|
+
return async (req) => {
|
|
36
|
+
const caller = req.user?.email;
|
|
37
|
+
const groupId = await resolveGroupId(req);
|
|
38
|
+
if (!groupId)
|
|
39
|
+
return caller;
|
|
40
|
+
const owner = await (0, client_1.getGroupOwner)(String(groupId));
|
|
41
|
+
if (owner)
|
|
42
|
+
return owner.ownerEmail;
|
|
43
|
+
logger_1.default.error('Group owner unresolved; billing usage to the caller instead', {
|
|
44
|
+
event: 'entitlement.subject_fallback',
|
|
45
|
+
groupId: String(groupId),
|
|
46
|
+
caller,
|
|
47
|
+
route: req.originalUrl
|
|
48
|
+
});
|
|
49
|
+
return caller;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=subject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subject.js","sourceRoot":"","sources":["../../src/groups/subject.ts"],"names":[],"mappings":";;;;;AAgCA,8CAiBC;AAhDD,+DAAuC;AAEvC,qCAAyC;AAIzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,SAAgB,iBAAiB,CAAC,cAA+B;IAC/D,OAAO,KAAK,EAAE,GAAY,EAAE,EAAE;QAC5B,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC;QAC/B,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAI,CAAC,OAAO;YAAE,OAAO,MAAM,CAAC;QAE5B,MAAM,KAAK,GAAG,MAAM,IAAA,sBAAa,EAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QACnD,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC,UAAU,CAAC;QAEnC,gBAAM,CAAC,KAAK,CAAC,6DAA6D,EAAE;YAC1E,KAAK,EAAE,8BAA8B;YACrC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC;YACxB,MAAM;YACN,KAAK,EAAE,GAAG,CAAC,WAAW;SACvB,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { requireEnv } from './config/env';
|
|
|
11
11
|
export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, TooManyRequestsError, PaymentRequiredError } from './errors/HttpError';
|
|
12
12
|
export type { PaymentRequiredDetails } from './errors/HttpError';
|
|
13
13
|
export * from './entitlements';
|
|
14
|
+
export * from './groups';
|
|
14
15
|
export { authenticateToken } from './middleware/authMiddleware';
|
|
15
16
|
export { requireInternalServiceToken, internalServiceTokenBypass, allowUserOrInternalService, verifyInternalToken, INTERNAL_TOKEN_HEADER, SERVICE_ID_HEADER } from './middleware/internalServiceAuth';
|
|
16
17
|
export { createCorsMiddleware } from './middleware/corsMiddleware';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC7K,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAGjE,cAAc,gBAAgB,CAAC;AAG/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC1G,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACnH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,UAAU,EACV,aAAa,EACb,kBAAkB,EACnB,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC7K,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAGjE,cAAc,gBAAgB,CAAC;AAG/B,cAAc,UAAU,CAAC;AAGzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC1G,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACnH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,UAAU,EACV,aAAa,EACb,kBAAkB,EACnB,MAAM,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,8 @@ Object.defineProperty(exports, "TooManyRequestsError", { enumerable: true, get:
|
|
|
54
54
|
Object.defineProperty(exports, "PaymentRequiredError", { enumerable: true, get: function () { return HttpError_1.PaymentRequiredError; } });
|
|
55
55
|
// Entitlements (plan limits, usage meters, enforcement)
|
|
56
56
|
__exportStar(require("./entitlements"), exports);
|
|
57
|
+
// Group ownership (the quota subject for anything pooled)
|
|
58
|
+
__exportStar(require("./groups"), exports);
|
|
57
59
|
// Middleware
|
|
58
60
|
var authMiddleware_1 = require("./middleware/authMiddleware");
|
|
59
61
|
Object.defineProperty(exports, "authenticateToken", { enumerable: true, get: function () { return authMiddleware_1.authenticateToken; } });
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,qFAAqF;;;;;;;;;;;;;;;;;;;;;AAErF,gBAAgB;AAChB,qDAAoD;AAA3C,8GAAA,aAAa,OAAA;AAEtB,2CAAkD;AAAzC,4GAAA,gBAAgB,OAAA;AAGzB,UAAU;AACV,2CAAqD;AAA5C,iHAAA,OAAO,OAAU;AAE1B,gBAAgB;AAChB,8DAAoF;AAA3E,+GAAA,WAAW,OAAA;AAAE,8GAAA,UAAU,OAAA;AAAE,kHAAA,cAAc,OAAA;AAChD,gEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAG1B,WAAW;AACX,oDAAgE;AAAvD,uGAAA,SAAS,OAAA;AAAE,0GAAA,YAAY,OAAA;AAEhC,SAAS;AACT,oCAA0C;AAAjC,iGAAA,UAAU,OAAA;AAEnB,SAAS;AACT,gDAA6K;AAApK,sGAAA,SAAS,OAAA;AAAE,4GAAA,eAAe,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAAE,2GAAA,cAAc,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAGhJ,wDAAwD;AACxD,iDAA+B;AAE/B,aAAa;AACb,8DAAgE;AAAvD,mHAAA,iBAAiB,OAAA;AAC1B,wEAO0C;AANxC,kIAAA,2BAA2B,OAAA;AAC3B,iIAAA,0BAA0B,OAAA;AAC1B,iIAAA,0BAA0B,OAAA;AAC1B,0HAAA,mBAAmB,OAAA;AACnB,4HAAA,qBAAqB,OAAA;AACrB,wHAAA,iBAAiB,OAAA;AAEnB,8DAAmE;AAA1D,sHAAA,oBAAoB,OAAA;AAC7B,0DAAyD;AAAhD,4GAAA,YAAY,OAAA;AACrB,4DAA0G;AAAjG,8GAAA,aAAa,OAAA;AAAE,yHAAA,wBAAwB,OAAA;AAAE,oHAAA,mBAAmB,OAAA;AACrE,kDAAmH;AAA1G,2GAAA,eAAe,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AAAE,+GAAA,mBAAmB,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AACnF,kDAAiD;AAAxC,oGAAA,QAAQ,OAAA;AAKjB,QAAQ;AACR,mDAAmF;AAA1E,oHAAA,qBAAqB,OAAA;AAAE,oHAAA,qBAAqB,OAAA;AACrD,6CAA6E;AAApE,6GAAA,iBAAiB,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,qGAAA,SAAS,OAAA;AAElD,2DAA8E;AAArE,mHAAA,gBAAgB,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAE5C,UAAU;AACV,qCAQmB;AAPjB,4GAAA,iBAAiB,OAAA;AACjB,8GAAA,mBAAmB,OAAA;AACnB,4GAAA,iBAAiB,OAAA;AACjB,gHAAA,qBAAqB,OAAA;AACrB,+GAAA,oBAAoB,OAAA;AACpB,0GAAA,eAAe,OAAA;AACf,mGAAA,QAAQ,OAAA;AAGV,UAAU;AACV,qCAUmB;AATjB,qGAAA,UAAU,OAAA;AACV,oGAAA,SAAS,OAAA;AACT,oGAAA,SAAS,OAAA;AACT,4GAAA,iBAAiB,OAAA;AACjB,0GAAA,eAAe,OAAA;AACf,oGAAA,SAAS,OAAA;AACT,qGAAA,UAAU,OAAA;AACV,wGAAA,aAAa,OAAA;AACb,6GAAA,kBAAkB,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,qFAAqF;;;;;;;;;;;;;;;;;;;;;AAErF,gBAAgB;AAChB,qDAAoD;AAA3C,8GAAA,aAAa,OAAA;AAEtB,2CAAkD;AAAzC,4GAAA,gBAAgB,OAAA;AAGzB,UAAU;AACV,2CAAqD;AAA5C,iHAAA,OAAO,OAAU;AAE1B,gBAAgB;AAChB,8DAAoF;AAA3E,+GAAA,WAAW,OAAA;AAAE,8GAAA,UAAU,OAAA;AAAE,kHAAA,cAAc,OAAA;AAChD,gEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAG1B,WAAW;AACX,oDAAgE;AAAvD,uGAAA,SAAS,OAAA;AAAE,0GAAA,YAAY,OAAA;AAEhC,SAAS;AACT,oCAA0C;AAAjC,iGAAA,UAAU,OAAA;AAEnB,SAAS;AACT,gDAA6K;AAApK,sGAAA,SAAS,OAAA;AAAE,4GAAA,eAAe,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAAE,2GAAA,cAAc,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,0GAAA,aAAa,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAAE,iHAAA,oBAAoB,OAAA;AAGhJ,wDAAwD;AACxD,iDAA+B;AAE/B,0DAA0D;AAC1D,2CAAyB;AAEzB,aAAa;AACb,8DAAgE;AAAvD,mHAAA,iBAAiB,OAAA;AAC1B,wEAO0C;AANxC,kIAAA,2BAA2B,OAAA;AAC3B,iIAAA,0BAA0B,OAAA;AAC1B,iIAAA,0BAA0B,OAAA;AAC1B,0HAAA,mBAAmB,OAAA;AACnB,4HAAA,qBAAqB,OAAA;AACrB,wHAAA,iBAAiB,OAAA;AAEnB,8DAAmE;AAA1D,sHAAA,oBAAoB,OAAA;AAC7B,0DAAyD;AAAhD,4GAAA,YAAY,OAAA;AACrB,4DAA0G;AAAjG,8GAAA,aAAa,OAAA;AAAE,yHAAA,wBAAwB,OAAA;AAAE,oHAAA,mBAAmB,OAAA;AACrE,kDAAmH;AAA1G,2GAAA,eAAe,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AAAE,+GAAA,mBAAmB,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AACnF,kDAAiD;AAAxC,oGAAA,QAAQ,OAAA;AAKjB,QAAQ;AACR,mDAAmF;AAA1E,oHAAA,qBAAqB,OAAA;AAAE,oHAAA,qBAAqB,OAAA;AACrD,6CAA6E;AAApE,6GAAA,iBAAiB,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,qGAAA,SAAS,OAAA;AAElD,2DAA8E;AAArE,mHAAA,gBAAgB,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAE5C,UAAU;AACV,qCAQmB;AAPjB,4GAAA,iBAAiB,OAAA;AACjB,8GAAA,mBAAmB,OAAA;AACnB,4GAAA,iBAAiB,OAAA;AACjB,gHAAA,qBAAqB,OAAA;AACrB,+GAAA,oBAAoB,OAAA;AACpB,0GAAA,eAAe,OAAA;AACf,mGAAA,QAAQ,OAAA;AAGV,UAAU;AACV,qCAUmB;AATjB,qGAAA,UAAU,OAAA;AACV,oGAAA,SAAS,OAAA;AACT,oGAAA,SAAS,OAAA;AACT,4GAAA,iBAAiB,OAAA;AACjB,0GAAA,eAAe,OAAA;AACf,oGAAA,SAAS,OAAA;AACT,qGAAA,UAAU,OAAA;AACV,wGAAA,aAAa,OAAA;AACb,6GAAA,kBAAkB,OAAA"}
|
package/package.json
CHANGED
|
@@ -150,6 +150,36 @@ describe('when payment-service cannot be reached', () => {
|
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
describe('base URL selection', () => {
|
|
154
|
+
it('prefers the container-network address over the public hostname', async () => {
|
|
155
|
+
process.env.PAYMENT_INTERNAL_URL = 'http://payment-service:5005';
|
|
156
|
+
process.env.PAYMENT_API_URL = 'https://payment-api.tumbaland.eu';
|
|
157
|
+
(global.fetch as jest.Mock).mockResolvedValue(paidResponse());
|
|
158
|
+
|
|
159
|
+
await getEntitlements('user@example.com');
|
|
160
|
+
|
|
161
|
+
// payment-front templates the public variable into the browser's
|
|
162
|
+
// config.json, so it cannot be pointed inward — hence the second variable.
|
|
163
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
164
|
+
'http://payment-service:5005/internal/entitlements?email=user%40example.com',
|
|
165
|
+
expect.anything()
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('falls back to the public URL, so local dev and un-migrated deploys still work', async () => {
|
|
170
|
+
delete process.env.PAYMENT_INTERNAL_URL;
|
|
171
|
+
process.env.PAYMENT_API_URL = 'https://payment-api.tumbaland.eu';
|
|
172
|
+
(global.fetch as jest.Mock).mockResolvedValue(paidResponse());
|
|
173
|
+
|
|
174
|
+
await getEntitlements('user@example.com');
|
|
175
|
+
|
|
176
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
177
|
+
'https://payment-api.tumbaland.eu/internal/entitlements?email=user%40example.com',
|
|
178
|
+
expect.anything()
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
153
183
|
describe('cache bounds', () => {
|
|
154
184
|
it('evicts the oldest entries instead of growing without limit', async () => {
|
|
155
185
|
(global.fetch as jest.Mock).mockResolvedValue(paidResponse());
|
|
@@ -72,8 +72,25 @@ interface EntitlementResponse {
|
|
|
72
72
|
upgradeTo?: string;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Prefers the container-network address over the public hostname.
|
|
77
|
+
*
|
|
78
|
+
* `PAYMENT_API_URL` cannot simply be pointed inward: payment-front's entrypoint
|
|
79
|
+
* templates that same variable into the `config.json` the *browser* downloads,
|
|
80
|
+
* so a Docker-internal value there breaks the subscription and checkout pages.
|
|
81
|
+
* One variable, two consumers with incompatible needs — this is the server-side
|
|
82
|
+
* half of it.
|
|
83
|
+
*
|
|
84
|
+
* Falling back keeps local dev (where only `PAYMENT_API_URL` is set) and any
|
|
85
|
+
* un-migrated deployment working; it just takes the long way round, out through
|
|
86
|
+
* the proxy and back, carrying the internal token across the public edge.
|
|
87
|
+
*/
|
|
88
|
+
function internalPaymentUrl(): string | undefined {
|
|
89
|
+
return process.env.PAYMENT_INTERNAL_URL || process.env.PAYMENT_API_URL;
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
async function fetchEntitlements(email: string): Promise<Entitlements> {
|
|
76
|
-
const baseUrl =
|
|
93
|
+
const baseUrl = internalPaymentUrl();
|
|
77
94
|
const token = process.env.INTERNAL_SERVICE_TOKEN;
|
|
78
95
|
const serviceId = process.env.SERVICE_NAME || 'unknown-service';
|
|
79
96
|
|
|
@@ -26,14 +26,16 @@ export type {
|
|
|
26
26
|
PartialPlanLimits
|
|
27
27
|
} from './definitions';
|
|
28
28
|
|
|
29
|
-
export type { Entitlements
|
|
30
|
-
export { getEntitlementsMode, resetEntitlementsModeWarning } from './mode';
|
|
29
|
+
export type { Entitlements } from './types';
|
|
31
30
|
|
|
32
31
|
export { UsageMeter, currentPeriod } from './UsageMeter';
|
|
33
32
|
export type { IUsageMeter } from './UsageMeter';
|
|
34
33
|
export { getUsage, getUsageSnapshot, checkQuota, consumeQuota, releaseQuota, setUsage } from './usage';
|
|
35
34
|
export type { QuotaRequest } from './usage';
|
|
36
35
|
|
|
36
|
+
export { reconcileUsageMeters, runUsageReconciliation, RECONCILED_METERS } from './reconcile';
|
|
37
|
+
export type { ReconcileOptions, ReconcileReport, MeterChange, OrphanedMeter } from './reconcile';
|
|
38
|
+
|
|
37
39
|
export { getEntitlements, invalidateEntitlements, clearEntitlementsCache } from './client';
|
|
38
40
|
|
|
39
41
|
export { loadEntitlements, requireEntitlement, requireQuota, retentionFloor } from './middleware';
|
|
@@ -35,7 +35,7 @@ function mockReq(overrides: Partial<Request> = {}): Request {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
beforeEach(() => {
|
|
38
|
-
process.env = { ...ORIGINAL_ENV
|
|
38
|
+
process.env = { ...ORIGINAL_ENV };
|
|
39
39
|
jest.clearAllMocks();
|
|
40
40
|
mockGetEntitlements.mockResolvedValue(entitlements());
|
|
41
41
|
});
|
|
@@ -89,24 +89,10 @@ describe('requireEntitlement', () => {
|
|
|
89
89
|
);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
it('logs
|
|
93
|
-
|
|
94
|
-
const next = jest.fn();
|
|
95
|
-
|
|
96
|
-
await requireEntitlement('cleanExport')(mockReq(), {} as Response, next);
|
|
97
|
-
|
|
98
|
-
expect(next).toHaveBeenCalledWith();
|
|
99
|
-
expect(logger.info).toHaveBeenCalledWith('entitlement.would_block', expect.objectContaining({ feature: 'cleanExport' }));
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('does not even look entitlements up when the mode is off', async () => {
|
|
103
|
-
process.env.ENTITLEMENTS_MODE = 'off';
|
|
104
|
-
const next = jest.fn();
|
|
105
|
-
|
|
106
|
-
await requireEntitlement('cleanExport')(mockReq(), {} as Response, next);
|
|
92
|
+
it('logs the denial alongside throwing it', async () => {
|
|
93
|
+
await requireEntitlement('cleanExport')(mockReq(), {} as Response, jest.fn());
|
|
107
94
|
|
|
108
|
-
expect(
|
|
109
|
-
expect(mockGetEntitlements).not.toHaveBeenCalled();
|
|
95
|
+
expect(logger.info).toHaveBeenCalledWith('entitlement.blocked', expect.objectContaining({ feature: 'cleanExport' }));
|
|
110
96
|
});
|
|
111
97
|
});
|
|
112
98
|
|
|
@@ -143,15 +129,6 @@ describe('requireQuota', () => {
|
|
|
143
129
|
expect(next).toHaveBeenCalledWith(denial);
|
|
144
130
|
});
|
|
145
131
|
|
|
146
|
-
it('is inert when the mode is off', async () => {
|
|
147
|
-
process.env.ENTITLEMENTS_MODE = 'off';
|
|
148
|
-
const next = jest.fn();
|
|
149
|
-
|
|
150
|
-
await requireQuota('storageBytes')(mockReq(), {} as Response, next);
|
|
151
|
-
|
|
152
|
-
expect(mockCheckQuota).not.toHaveBeenCalled();
|
|
153
|
-
expect(next).toHaveBeenCalledWith();
|
|
154
|
-
});
|
|
155
132
|
});
|
|
156
133
|
|
|
157
134
|
describe('retentionFloor', () => {
|
|
@@ -171,9 +148,11 @@ describe('retentionFloor', () => {
|
|
|
171
148
|
expect(await retentionFloor('user@example.com', now)).toBeNull();
|
|
172
149
|
});
|
|
173
150
|
|
|
174
|
-
it('
|
|
175
|
-
|
|
151
|
+
it('still applies the floor when the limits are a stale fallback', async () => {
|
|
152
|
+
mockGetEntitlements.mockResolvedValue(entitlements({ stale: true }));
|
|
176
153
|
|
|
177
|
-
|
|
154
|
+
// Fail closed, same as every other gate: an outage clips history to the
|
|
155
|
+
// free tier for a minute rather than handing the full archive out.
|
|
156
|
+
expect(await retentionFloor('user@example.com', now)).toEqual(new Date('2026-05-11T00:00:00.000Z'));
|
|
178
157
|
});
|
|
179
158
|
});
|