@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.
Files changed (59) hide show
  1. package/dist/entitlements/client.d.ts.map +1 -1
  2. package/dist/entitlements/client.js +17 -1
  3. package/dist/entitlements/client.js.map +1 -1
  4. package/dist/entitlements/index.d.ts +3 -2
  5. package/dist/entitlements/index.d.ts.map +1 -1
  6. package/dist/entitlements/index.js +5 -4
  7. package/dist/entitlements/index.js.map +1 -1
  8. package/dist/entitlements/middleware.d.ts.map +1 -1
  9. package/dist/entitlements/middleware.js +8 -19
  10. package/dist/entitlements/middleware.js.map +1 -1
  11. package/dist/entitlements/reconcile.d.ts +74 -0
  12. package/dist/entitlements/reconcile.d.ts.map +1 -0
  13. package/dist/entitlements/reconcile.js +297 -0
  14. package/dist/entitlements/reconcile.js.map +1 -0
  15. package/dist/entitlements/types.d.ts +0 -8
  16. package/dist/entitlements/types.d.ts.map +1 -1
  17. package/dist/entitlements/usage.d.ts +1 -1
  18. package/dist/entitlements/usage.d.ts.map +1 -1
  19. package/dist/entitlements/usage.js +11 -20
  20. package/dist/entitlements/usage.js.map +1 -1
  21. package/dist/groups/client.d.ts +27 -0
  22. package/dist/groups/client.d.ts.map +1 -0
  23. package/dist/groups/client.js +153 -0
  24. package/dist/groups/client.js.map +1 -0
  25. package/dist/groups/index.d.ts +5 -0
  26. package/dist/groups/index.d.ts.map +1 -0
  27. package/dist/groups/index.js +10 -0
  28. package/dist/groups/index.js.map +1 -0
  29. package/dist/groups/subject.d.ts +30 -0
  30. package/dist/groups/subject.d.ts.map +1 -0
  31. package/dist/groups/subject.js +52 -0
  32. package/dist/groups/subject.js.map +1 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/index.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/entitlements/client.test.ts +30 -0
  39. package/src/entitlements/client.ts +18 -1
  40. package/src/entitlements/index.ts +4 -2
  41. package/src/entitlements/middleware.test.ts +9 -30
  42. package/src/entitlements/middleware.ts +8 -19
  43. package/src/entitlements/reconcile.test.ts +333 -0
  44. package/src/entitlements/reconcile.ts +384 -0
  45. package/src/entitlements/types.ts +0 -9
  46. package/src/entitlements/usage.test.ts +27 -31
  47. package/src/entitlements/usage.ts +12 -22
  48. package/src/groups/client.test.ts +215 -0
  49. package/src/groups/client.ts +182 -0
  50. package/src/groups/index.ts +5 -0
  51. package/src/groups/subject.test.ts +85 -0
  52. package/src/groups/subject.ts +50 -0
  53. package/src/index.ts +3 -0
  54. package/dist/entitlements/mode.d.ts +0 -10
  55. package/dist/entitlements/mode.d.ts.map +0 -1
  56. package/dist/entitlements/mode.js +0 -32
  57. package/dist/entitlements/mode.js.map +0 -1
  58. package/src/entitlements/mode.test.ts +0 -50
  59. 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;AAGvC,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,QAAQ,CAAC;IAChB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,YAAY,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,sDAAsD;AACtD,wBAAsB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAO9F;AAED,sFAAsF;AACtF,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAYvG;AA2BD;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAejE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BnE;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,QAAQ,EACf,MAAM,SAAI,EACV,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE,MAAM,EACb,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,IAAI,CAAC,CAMf"}
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
- function logWouldBlock(req, used, limit) {
51
- logger_1.default.info('entitlement.would_block', {
52
- event: 'entitlement.would_block',
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
- if (mode === 'observe') {
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
- if (mode === 'observe') {
114
- // Keep the increment: observe mode exists to measure real demand, and a
115
- // rolled-back count would under-report exactly the users who matter.
116
- logWouldBlock(req, usedBefore, limit);
117
- return;
118
- }
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":"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"}
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,5 @@
1
+ export { getGroupOwner, invalidateGroupOwner, clearGroupOwnerCache } from './client';
2
+ export type { GroupOwner } from './client';
3
+ export { groupOwnerSubject } from './subject';
4
+ export type { GroupIdResolver } from './subject';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tumbaland/backend-core",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "description": "Core shared functionality for Tumbaland backend services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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 = process.env.PAYMENT_API_URL;
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, EntitlementsMode } from './types';
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, ENTITLEMENTS_MODE: 'enforce' };
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 instead of blocking in observe mode', async () => {
93
- process.env.ENTITLEMENTS_MODE = 'observe';
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(next).toHaveBeenCalledWith();
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('never truncates history outside enforce mode', async () => {
175
- process.env.ENTITLEMENTS_MODE = 'observe';
151
+ it('still applies the floor when the limits are a stale fallback', async () => {
152
+ mockGetEntitlements.mockResolvedValue(entitlements({ stale: true }));
176
153
 
177
- expect(await retentionFloor('user@example.com', now)).toBeNull();
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
  });