@tumbaland/backend-core 1.23.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/app/createBaseApp.d.ts +24 -0
  2. package/dist/app/createBaseApp.d.ts.map +1 -1
  3. package/dist/app/createBaseApp.js +20 -1
  4. package/dist/app/createBaseApp.js.map +1 -1
  5. package/dist/entitlements/UsageMeter.d.ts +30 -0
  6. package/dist/entitlements/UsageMeter.d.ts.map +1 -0
  7. package/dist/entitlements/UsageMeter.js +27 -0
  8. package/dist/entitlements/UsageMeter.js.map +1 -0
  9. package/dist/entitlements/client.d.ts +17 -0
  10. package/dist/entitlements/client.d.ts.map +1 -0
  11. package/dist/entitlements/client.js +142 -0
  12. package/dist/entitlements/client.js.map +1 -0
  13. package/dist/entitlements/definitions.d.ts +124 -0
  14. package/dist/entitlements/definitions.d.ts.map +1 -0
  15. package/dist/entitlements/definitions.js +210 -0
  16. package/dist/entitlements/definitions.js.map +1 -0
  17. package/dist/entitlements/index.d.ts +12 -0
  18. package/dist/entitlements/index.d.ts.map +1 -0
  19. package/dist/entitlements/index.js +43 -0
  20. package/dist/entitlements/index.js.map +1 -0
  21. package/dist/entitlements/middleware.d.ts +54 -0
  22. package/dist/entitlements/middleware.d.ts.map +1 -0
  23. package/dist/entitlements/middleware.js +109 -0
  24. package/dist/entitlements/middleware.js.map +1 -0
  25. package/dist/entitlements/mode.d.ts +10 -0
  26. package/dist/entitlements/mode.d.ts.map +1 -0
  27. package/dist/entitlements/mode.js +32 -0
  28. package/dist/entitlements/mode.js.map +1 -0
  29. package/dist/entitlements/types.d.ts +29 -0
  30. package/dist/entitlements/types.d.ts.map +1 -0
  31. package/dist/entitlements/types.js +3 -0
  32. package/dist/entitlements/types.js.map +1 -0
  33. package/dist/entitlements/usage.d.ts +56 -0
  34. package/dist/entitlements/usage.d.ts.map +1 -0
  35. package/dist/entitlements/usage.js +159 -0
  36. package/dist/entitlements/usage.js.map +1 -0
  37. package/dist/errors/HttpError.d.ts +30 -1
  38. package/dist/errors/HttpError.d.ts.map +1 -1
  39. package/dist/errors/HttpError.js +20 -2
  40. package/dist/errors/HttpError.js.map +1 -1
  41. package/dist/index.d.ts +4 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +26 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/middleware/errorHandler.d.ts.map +1 -1
  46. package/dist/middleware/errorHandler.js +4 -1
  47. package/dist/middleware/errorHandler.js.map +1 -1
  48. package/dist/middleware/internalServiceAuth.d.ts +44 -0
  49. package/dist/middleware/internalServiceAuth.d.ts.map +1 -0
  50. package/dist/middleware/internalServiceAuth.js +86 -0
  51. package/dist/middleware/internalServiceAuth.js.map +1 -0
  52. package/package.json +2 -1
  53. package/src/app/createBaseApp.test.ts +31 -0
  54. package/src/app/createBaseApp.ts +28 -1
  55. package/src/entitlements/UsageMeter.ts +49 -0
  56. package/src/entitlements/client.test.ts +170 -0
  57. package/src/entitlements/client.ts +162 -0
  58. package/src/entitlements/definitions.test.ts +161 -0
  59. package/src/entitlements/definitions.ts +261 -0
  60. package/src/entitlements/index.ts +40 -0
  61. package/src/entitlements/middleware.test.ts +179 -0
  62. package/src/entitlements/middleware.ts +136 -0
  63. package/src/entitlements/mode.test.ts +50 -0
  64. package/src/entitlements/mode.ts +28 -0
  65. package/src/entitlements/types.ts +30 -0
  66. package/src/entitlements/usage.test.ts +271 -0
  67. package/src/entitlements/usage.ts +189 -0
  68. package/src/errors/HttpError.test.ts +39 -1
  69. package/src/errors/HttpError.ts +35 -1
  70. package/src/index.ts +13 -1
  71. package/src/middleware/errorHandler.test.ts +43 -1
  72. package/src/middleware/errorHandler.ts +6 -1
  73. package/src/middleware/internalServiceAuth.test.ts +173 -0
  74. package/src/middleware/internalServiceAuth.ts +96 -0
@@ -18,6 +18,12 @@ export interface CreateBaseAppOptions {
18
18
  tracing?: boolean;
19
19
  /** Use requestLoggerWithMetrics (adds Prometheus histograms) instead of the plain requestLogger. Default true. */
20
20
  metrics?: boolean;
21
+ /**
22
+ * Value for Express's `trust proxy`. Defaults to trusting private/loopback
23
+ * addresses — see the note on tumbaland-proxy below. Pass `false` for a
24
+ * service exposed directly to clients with no proxy in front of it.
25
+ */
26
+ trustProxy?: Parameters<Express['set']>[1];
21
27
  }
22
28
  /**
23
29
  * Assembles the middleware stack every service was hand-rolling in its own
@@ -39,6 +45,24 @@ export interface CreateBaseAppOptions {
39
45
  * Services with non-standard body parsing (a webhook needing the raw body,
40
46
  * or conditional parsing per-route) should pass `parseJson`/`parseCookies:
41
47
  * false` and mount those themselves at the exact point they need to.
48
+ *
49
+ * `trust proxy` is on by default because nothing here is reached directly:
50
+ * every service sits behind tumbaland-proxy, which sets `X-Forwarded-For`.
51
+ * Left at Express's default (`false`) that header is ignored, which
52
+ * express-rate-limit reports as `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR` — and
53
+ * it is right to: the limiter would then key every request in the cluster on
54
+ * the proxy's own container IP, so one busy client could exhaust the window
55
+ * for everyone.
56
+ *
57
+ * The value is `loopback, linklocal, uniquelocal` (private ranges) rather
58
+ * than a hop count, because the chain length differs per ingress path: a
59
+ * direct hit arrives as `client` + the proxy's own socket address, while
60
+ * traffic through the Cloudflare tunnel arrives as `client, 127.0.0.1` (the
61
+ * local cloudflared) + that same socket address. Express walks the chain
62
+ * right-to-left and stops at the first address it does not trust, so both
63
+ * paths land on the real client; a fixed number would be wrong for one of
64
+ * them. All the intermediaries are private addresses and the client is not,
65
+ * which is exactly the split this preset encodes.
42
66
  */
43
67
  export declare function createBaseApp(options?: CreateBaseAppOptions): Express;
44
68
  //# sourceMappingURL=createBaseApp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createBaseApp.d.ts","sourceRoot":"","sources":["../../src/app/createBaseApp.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK3D,OAAO,EAAwB,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAI3F,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,cAAc,GAAG,KAAK,CAAC;IACrC,4IAA4I;IAC5I,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kHAAkH;IAClH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAwBzE"}
1
+ {"version":3,"file":"createBaseApp.d.ts","sourceRoot":"","sources":["../../src/app/createBaseApp.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK3D,OAAO,EAAwB,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAI3F,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,cAAc,GAAG,KAAK,CAAC;IACrC,4IAA4I;IAC5I,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kHAAkH;IAClH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CA2BzE"}
@@ -32,10 +32,29 @@ const healthController_1 = require("../health/healthController");
32
32
  * Services with non-standard body parsing (a webhook needing the raw body,
33
33
  * or conditional parsing per-route) should pass `parseJson`/`parseCookies:
34
34
  * false` and mount those themselves at the exact point they need to.
35
+ *
36
+ * `trust proxy` is on by default because nothing here is reached directly:
37
+ * every service sits behind tumbaland-proxy, which sets `X-Forwarded-For`.
38
+ * Left at Express's default (`false`) that header is ignored, which
39
+ * express-rate-limit reports as `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR` — and
40
+ * it is right to: the limiter would then key every request in the cluster on
41
+ * the proxy's own container IP, so one busy client could exhaust the window
42
+ * for everyone.
43
+ *
44
+ * The value is `loopback, linklocal, uniquelocal` (private ranges) rather
45
+ * than a hop count, because the chain length differs per ingress path: a
46
+ * direct hit arrives as `client` + the proxy's own socket address, while
47
+ * traffic through the Cloudflare tunnel arrives as `client, 127.0.0.1` (the
48
+ * local cloudflared) + that same socket address. Express walks the chain
49
+ * right-to-left and stops at the first address it does not trust, so both
50
+ * paths land on the real client; a fixed number would be wrong for one of
51
+ * them. All the intermediaries are private addresses and the client is not,
52
+ * which is exactly the split this preset encodes.
35
53
  */
36
54
  function createBaseApp(options = {}) {
37
- const { corsOptions, rateLimiter = security_1.standardRateLimiter, parseJson = true, parseCookies = true, tracing = true, metrics = true } = options;
55
+ const { corsOptions, rateLimiter = security_1.standardRateLimiter, parseJson = true, parseCookies = true, tracing = true, metrics = true, trustProxy = 'loopback, linklocal, uniquelocal' } = options;
38
56
  const app = (0, express_1.default)();
57
+ app.set('trust proxy', trustProxy);
39
58
  app.get('/health/live', healthController_1.healthLive);
40
59
  app.use(security_1.securityHeaders);
41
60
  if (rateLimiter)
@@ -1 +1 @@
1
- {"version":3,"file":"createBaseApp.js","sourceRoot":"","sources":["../../src/app/createBaseApp.ts"],"names":[],"mappings":";;;;;AAkDA,sCAwBC;AA1ED,sDAA2D;AAC3D,kEAAyC;AACzC,sDAA6D;AAC7D,wCAA+C;AAC/C,+DAAsF;AACtF,iEAA2F;AAC3F,qDAA8E;AAC9E,iEAAwD;AAsBxD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,aAAa,CAAC,UAAgC,EAAE;IAC9D,MAAM,EACJ,WAAW,EACX,WAAW,GAAG,8BAAmB,EACjC,SAAS,GAAG,IAAI,EAChB,YAAY,GAAG,IAAI,EACnB,OAAO,GAAG,IAAI,EACd,OAAO,GAAG,IAAI,EACf,GAAG,OAAO,CAAC;IAEZ,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IAEtB,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,6BAAU,CAAC,CAAC;IAEpC,GAAG,CAAC,GAAG,CAAC,0BAAe,CAAC,CAAC;IACzB,IAAI,WAAW;QAAE,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACtC,GAAG,CAAC,GAAG,CAAC,mCAAqB,CAAC,CAAC;IAC/B,IAAI,OAAO;QAAE,GAAG,CAAC,GAAG,CAAC,2BAAiB,CAAC,CAAC;IACxC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,wCAAwB,CAAC,CAAC,CAAC,6BAAa,CAAC,CAAC;IAC5D,GAAG,CAAC,GAAG,CAAC,IAAA,qCAAoB,EAAC,WAAW,CAAC,CAAC,CAAC;IAC3C,IAAI,YAAY;QAAE,GAAG,CAAC,GAAG,CAAC,IAAA,uBAAY,GAAE,CAAC,CAAC;IAC1C,IAAI,SAAS;QAAE,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAEvC,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"createBaseApp.js","sourceRoot":"","sources":["../../src/app/createBaseApp.ts"],"names":[],"mappings":";;;;;AA0EA,sCA2BC;AArGD,sDAA2D;AAC3D,kEAAyC;AACzC,sDAA6D;AAC7D,wCAA+C;AAC/C,+DAAsF;AACtF,iEAA2F;AAC3F,qDAA8E;AAC9E,iEAAwD;AA4BxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,SAAgB,aAAa,CAAC,UAAgC,EAAE;IAC9D,MAAM,EACJ,WAAW,EACX,WAAW,GAAG,8BAAmB,EACjC,SAAS,GAAG,IAAI,EAChB,YAAY,GAAG,IAAI,EACnB,OAAO,GAAG,IAAI,EACd,OAAO,GAAG,IAAI,EACd,UAAU,GAAG,kCAAkC,EAChD,GAAG,OAAO,CAAC;IAEZ,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IAEtB,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;IAEnC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,6BAAU,CAAC,CAAC;IAEpC,GAAG,CAAC,GAAG,CAAC,0BAAe,CAAC,CAAC;IACzB,IAAI,WAAW;QAAE,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACtC,GAAG,CAAC,GAAG,CAAC,mCAAqB,CAAC,CAAC;IAC/B,IAAI,OAAO;QAAE,GAAG,CAAC,GAAG,CAAC,2BAAiB,CAAC,CAAC;IACxC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,wCAAwB,CAAC,CAAC,CAAC,6BAAa,CAAC,CAAC;IAC5D,GAAG,CAAC,GAAG,CAAC,IAAA,qCAAoB,EAAC,WAAW,CAAC,CAAC,CAAC;IAC3C,IAAI,YAAY;QAAE,GAAG,CAAC,GAAG,CAAC,IAAA,uBAAY,GAAE,CAAC,CAAC;IAC1C,IAAI,SAAS;QAAE,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAEvC,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { Document, Model } from 'mongoose';
2
+ import { MeterKey } from './definitions';
3
+ /**
4
+ * One counter per (subject, meter, period). Lives in the shared database, so
5
+ * album-service can write `storageBytes` while payment-service reads it back
6
+ * for the pricing page without another round trip.
7
+ *
8
+ * `subjectId` is whoever the usage is billed to — an email for personal
9
+ * meters, the group owner's email for pooled ones. It is deliberately not a
10
+ * user ObjectId: every service already has the email on the JWT, and none of
11
+ * them share a users collection.
12
+ */
13
+ export interface IUsageMeter extends Document {
14
+ subjectId: string;
15
+ meter: MeterKey;
16
+ /** `ALL` for cumulative meters, `YYYY-MM` for monthly ones. */
17
+ period: string;
18
+ count: number;
19
+ createdAt: Date;
20
+ updatedAt: Date;
21
+ }
22
+ /**
23
+ * Registered through `models` first: several services import this module into
24
+ * the same mongoose singleton during tests, and a second `model()` call with
25
+ * the same name throws `OverwriteModelError`.
26
+ */
27
+ export declare const UsageMeter: Model<IUsageMeter>;
28
+ /** The period bucket a meter counts into right now. Monthly meters roll over at 00:00 UTC on the 1st. */
29
+ export declare function currentPeriod(meter: MeterKey, now?: Date): string;
30
+ //# sourceMappingURL=UsageMeter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UsageMeter.d.ts","sourceRoot":"","sources":["../../src/entitlements/UsageMeter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAClE,OAAO,EAAE,QAAQ,EAA4B,MAAM,eAAe,CAAC;AAEnE;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,QAAQ,CAAC;IAChB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAcD;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,WAAW,CACuD,CAAC;AAElG,yGAAyG;AACzG,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAI7E"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UsageMeter = void 0;
4
+ exports.currentPeriod = currentPeriod;
5
+ const mongoose_1 = require("mongoose");
6
+ const definitions_1 = require("./definitions");
7
+ const UsageMeterSchema = new mongoose_1.Schema({
8
+ subjectId: { type: String, required: true, lowercase: true, trim: true },
9
+ meter: { type: String, required: true },
10
+ period: { type: String, required: true, default: 'ALL' },
11
+ count: { type: Number, required: true, default: 0 }
12
+ }, { timestamps: true });
13
+ UsageMeterSchema.index({ subjectId: 1, meter: 1, period: 1 }, { unique: true });
14
+ /**
15
+ * Registered through `models` first: several services import this module into
16
+ * the same mongoose singleton during tests, and a second `model()` call with
17
+ * the same name throws `OverwriteModelError`.
18
+ */
19
+ exports.UsageMeter = mongoose_1.models.UsageMeter || (0, mongoose_1.model)('UsageMeter', UsageMeterSchema);
20
+ /** The period bucket a meter counts into right now. Monthly meters roll over at 00:00 UTC on the 1st. */
21
+ function currentPeriod(meter, now = new Date()) {
22
+ const period = (0, definitions_1.meterPeriod)(meter);
23
+ if (period === 'ALL')
24
+ return 'ALL';
25
+ return now.toISOString().slice(0, 7); // YYYY-MM
26
+ }
27
+ //# sourceMappingURL=UsageMeter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UsageMeter.js","sourceRoot":"","sources":["../../src/entitlements/UsageMeter.ts"],"names":[],"mappings":";;;AA4CA,sCAIC;AAhDD,uCAAkE;AAClE,+CAAmE;AAsBnE,MAAM,gBAAgB,GAAG,IAAI,iBAAM,CACjC;IACE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IACxE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE;IACxD,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE;CACpD,EACD,EAAE,UAAU,EAAE,IAAI,EAAE,CACrB,CAAC;AAEF,gBAAgB,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAEhF;;;;GAIG;AACU,QAAA,UAAU,GACpB,iBAAM,CAAC,UAAiC,IAAI,IAAA,gBAAK,EAAc,YAAY,EAAE,gBAAgB,CAAC,CAAC;AAElG,yGAAyG;AACzG,SAAgB,aAAa,CAAC,KAAe,EAAE,MAAY,IAAI,IAAI,EAAE;IACnE,MAAM,MAAM,GAAgB,IAAA,yBAAW,EAAC,KAAK,CAAC,CAAC;IAC/C,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU;AAClD,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { Entitlements } from './types';
2
+ /**
3
+ * The user's current plan and limits, cached for {@link CACHE_TTL_MS}.
4
+ *
5
+ * Failures are never cached — a fallback result would otherwise pin a paying
6
+ * user to free limits for a full minute after the outage ended.
7
+ */
8
+ export declare function getEntitlements(email: string): Promise<Entitlements>;
9
+ /**
10
+ * Drop a user's cached entitlements. Call after anything that changes their
11
+ * plan (checkout completed, subscription canceled) to collapse the staleness
12
+ * window from 60s to zero for the user who is watching.
13
+ */
14
+ export declare function invalidateEntitlements(email: string): void;
15
+ /** Empties the whole cache. Test seam, and a usable lever after a bulk plan edit. */
16
+ export declare function clearEntitlementsCache(): void;
17
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/entitlements/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAsHvC;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAoB1E;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE1D;AAED,qFAAqF;AACrF,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C"}
@@ -0,0 +1,142 @@
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.getEntitlements = getEntitlements;
7
+ exports.invalidateEntitlements = invalidateEntitlements;
8
+ exports.clearEntitlementsCache = clearEntitlementsCache;
9
+ const logger_1 = __importDefault(require("../logging/logger"));
10
+ const definitions_1 = require("./definitions");
11
+ /**
12
+ * Entitlements are read over the internal-service channel, not carried on the
13
+ * JWT.
14
+ *
15
+ * The JWT is signed for 24h and no service outside auth-service checks
16
+ * `tokenVersion`, so a plan embedded in it would be stale in both directions:
17
+ * a user who upgrades waits up to a day for what they just paid for, and a user
18
+ * who cancels keeps paid features for up to a day. A 60-second cache in front
19
+ * of a live lookup bounds that to a minute, and the enforcement points are
20
+ * writes and dispatches — low frequency — so the network cost is negligible.
21
+ */
22
+ const CACHE_TTL_MS = 60_000;
23
+ /** Bounded so a service under enumeration can't grow the cache without limit. */
24
+ const MAX_CACHE_ENTRIES = 5_000;
25
+ const REQUEST_TIMEOUT_MS = 3_000;
26
+ const cache = new Map();
27
+ /**
28
+ * Concurrent callers for the same email share one request. Without this, a
29
+ * burst of uploads from one user on a cold cache fans out into one
30
+ * payment-service call each.
31
+ */
32
+ const inFlight = new Map();
33
+ function cacheKey(email) {
34
+ return email.trim().toLowerCase();
35
+ }
36
+ function putInCache(key, value) {
37
+ if (cache.size >= MAX_CACHE_ENTRIES) {
38
+ // Map iterates in insertion order, so the first key is the oldest write.
39
+ const oldest = cache.keys().next();
40
+ if (!oldest.done)
41
+ cache.delete(oldest.value);
42
+ }
43
+ cache.set(key, { value, expiresAt: Date.now() + CACHE_TTL_MS });
44
+ }
45
+ /**
46
+ * Fail closed: an unreachable payment-service degrades everyone to free limits
47
+ * rather than handing the product out for the length of the outage. The blast
48
+ * radius is bounded by design — reads are never gated, only writes and
49
+ * dispatches — so the worst case is "cannot upload for a minute", not
50
+ * "cannot use the app".
51
+ */
52
+ function fallbackEntitlements(email) {
53
+ return {
54
+ email,
55
+ planCode: definitions_1.FALLBACK_PLAN,
56
+ status: null,
57
+ limits: (0, definitions_1.resolveLimits)(definitions_1.FALLBACK_PLAN),
58
+ stale: true
59
+ };
60
+ }
61
+ async function fetchEntitlements(email) {
62
+ const baseUrl = process.env.PAYMENT_API_URL;
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('Entitlement lookup not configured', {
67
+ hasPaymentApiUrl: Boolean(baseUrl),
68
+ hasInternalToken: Boolean(token)
69
+ });
70
+ return fallbackEntitlements(email);
71
+ }
72
+ try {
73
+ const response = await fetch(`${baseUrl}/internal/entitlements?email=${encodeURIComponent(email)}`, {
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
+ if (!response.ok) {
83
+ logger_1.default.error('Entitlement lookup failed', { email, status: response.status });
84
+ return fallbackEntitlements(email);
85
+ }
86
+ const body = (await response.json());
87
+ const planCode = body.planCode || definitions_1.FALLBACK_PLAN;
88
+ return {
89
+ email,
90
+ planCode,
91
+ status: body.status ?? null,
92
+ // Re-normalized on this side too: the payload crossed a network boundary
93
+ // and a partially-configured plan must not leave a meter undefined.
94
+ limits: (0, definitions_1.normalizeLimits)(body.limits, planCode),
95
+ upgradeTo: body.upgradeTo
96
+ };
97
+ }
98
+ catch (err) {
99
+ logger_1.default.error('Entitlement lookup errored', { email, error: err?.message });
100
+ return fallbackEntitlements(email);
101
+ }
102
+ }
103
+ /**
104
+ * The user's current plan and limits, cached for {@link CACHE_TTL_MS}.
105
+ *
106
+ * Failures are never cached — a fallback result would otherwise pin a paying
107
+ * user to free limits for a full minute after the outage ended.
108
+ */
109
+ async function getEntitlements(email) {
110
+ const key = cacheKey(email);
111
+ const cached = cache.get(key);
112
+ if (cached && cached.expiresAt > Date.now())
113
+ return cached.value;
114
+ const pending = inFlight.get(key);
115
+ if (pending)
116
+ return pending;
117
+ const request = fetchEntitlements(key)
118
+ .then(value => {
119
+ if (!value.stale)
120
+ putInCache(key, value);
121
+ return value;
122
+ })
123
+ .finally(() => {
124
+ inFlight.delete(key);
125
+ });
126
+ inFlight.set(key, request);
127
+ return request;
128
+ }
129
+ /**
130
+ * Drop a user's cached entitlements. Call after anything that changes their
131
+ * plan (checkout completed, subscription canceled) to collapse the staleness
132
+ * window from 60s to zero for the user who is watching.
133
+ */
134
+ function invalidateEntitlements(email) {
135
+ cache.delete(cacheKey(email));
136
+ }
137
+ /** Empties the whole cache. Test seam, and a usable lever after a bulk plan edit. */
138
+ function clearEntitlementsCache() {
139
+ cache.clear();
140
+ inFlight.clear();
141
+ }
142
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/entitlements/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The entitlement *vocabulary*: which meters and features exist, what they
3
+ * mean, and what a plan gets when nothing else says otherwise.
4
+ *
5
+ * The actual per-plan numbers are NOT here — they live on the `Plan` document
6
+ * in Mongo and are editable by an admin at runtime (see payment-service's
7
+ * `Plan.limits` and `PUT /api/plans/:code/limits`). Shipping a code change to
8
+ * move a limit from 50 GB to 75 GB would be absurd, and a hardcoded table
9
+ * would immediately drift from what the pricing page renders.
10
+ *
11
+ * What code still owns:
12
+ * - the set of valid keys, so a typo in an admin form can't invent a meter
13
+ * that nothing enforces (`normalizeLimits` drops unknown keys);
14
+ * - `DEFAULT_PLAN_LIMITS`, the values used when a Plan document has no
15
+ * `limits` yet (fresh install, newly created plan) or when payment-service
16
+ * is unreachable and the client has to fail closed;
17
+ * - `ENTITLEMENT_CATALOG`, the metadata an admin UI renders its form from,
18
+ * so adding a meter needs no frontend change.
19
+ */
20
+ export type MeterKey = 'storageBytes' | 'aiJobs' | 'trackedTickers' | 'seats';
21
+ /**
22
+ * Deliberately small. The portfolio planner is NOT a feature flag — it is gated
23
+ * by `retentionDays` via the price-history range, because the planner runs
24
+ * entirely client-side and a boolean flag would be bypassable from devtools.
25
+ */
26
+ export type FeatureKey = 'cleanExport';
27
+ /** `ALL` = counts up forever (storage, seats). `MONTH` = resets each calendar month (UTC). */
28
+ export type MeterPeriod = 'ALL' | 'MONTH';
29
+ /** Sentinel for "no ceiling". Chosen over `Infinity` because it survives JSON and BSON. */
30
+ export declare const UNLIMITED = -1;
31
+ export interface MeterDefinition {
32
+ key: MeterKey;
33
+ label: string;
34
+ /** Drives formatting in the admin form and in `<UsageMeter>`: bytes get humanized, counts don't. */
35
+ unit: 'bytes' | 'count';
36
+ period: MeterPeriod;
37
+ description: string;
38
+ }
39
+ export interface FeatureDefinition {
40
+ key: FeatureKey;
41
+ label: string;
42
+ description: string;
43
+ }
44
+ export declare const METER_DEFINITIONS: readonly MeterDefinition[];
45
+ export declare const FEATURE_DEFINITIONS: readonly FeatureDefinition[];
46
+ export declare const METER_KEYS: readonly MeterKey[];
47
+ export declare const FEATURE_KEYS: readonly FeatureKey[];
48
+ export declare function meterPeriod(meter: MeterKey): MeterPeriod;
49
+ export declare function isMeterKey(value: string): value is MeterKey;
50
+ export declare function isFeatureKey(value: string): value is FeatureKey;
51
+ export interface PlanLimits {
52
+ /** `-1` (UNLIMITED) means no ceiling. */
53
+ meters: Record<MeterKey, number>;
54
+ features: Record<FeatureKey, boolean>;
55
+ /** How far back reads may reach. `-1` (UNLIMITED) means the full history. */
56
+ retentionDays: number;
57
+ }
58
+ /** What an admin form or an API caller may send: any subset, in any order. */
59
+ export interface PartialPlanLimits {
60
+ meters?: Partial<Record<string, number>>;
61
+ features?: Partial<Record<string, boolean>>;
62
+ retentionDays?: number;
63
+ }
64
+ /**
65
+ * Bootstrap values only — a Plan document's own `limits` wins over anything
66
+ * here. These are what a plan gets before an admin has ever touched it, and
67
+ * what the entitlement client falls back to when payment-service is down.
68
+ *
69
+ * Keep `free` conservative for exactly that reason: an outage degrades paying
70
+ * users to these numbers, and the alternative (fail-open) gives the product
71
+ * away for the length of the outage.
72
+ */
73
+ export declare const DEFAULT_PLAN_LIMITS: Readonly<Record<string, PlanLimits>>;
74
+ /** Plan code the seeded staff/comp plan uses. */
75
+ export declare const STAFF_PLAN = "staff";
76
+ export declare const FALLBACK_PLAN = "free";
77
+ /**
78
+ * Code defaults for a plan code, with unknown codes resolving to free.
79
+ *
80
+ * That fallback is deliberate: a typo'd or newly-added plan code must never
81
+ * silently grant unlimited. Callers that want the admin-configured numbers
82
+ * read them from the Plan document and pass them through `normalizeLimits`;
83
+ * this function is the floor underneath that.
84
+ */
85
+ export declare function resolveLimits(planCode?: string): PlanLimits;
86
+ /**
87
+ * Layers a partial set of limits over a complete one.
88
+ *
89
+ * Entitlements resolve through three layers, each one partial and each one
90
+ * overriding the last: code defaults → the Plan document's `limits` → a
91
+ * per-user `EntitlementOverride`. This is the single step all three share.
92
+ *
93
+ * - unknown meter/feature keys are dropped — no store can invent enforcement;
94
+ * - missing keys keep the base value, so adding a new meter in code works
95
+ * everywhere before any stored document mentions it;
96
+ * - negative numbers normalize to `UNLIMITED`, fractions floor.
97
+ */
98
+ export declare function applyLimits(base: PlanLimits, input?: PartialPlanLimits | null): PlanLimits;
99
+ /**
100
+ * Turns whatever is stored on a Plan document (or arrived from an admin form)
101
+ * into a complete, valid `PlanLimits`, filling the gaps from that plan code's
102
+ * code defaults.
103
+ */
104
+ export declare function normalizeLimits(input?: PartialPlanLimits | null, planCode?: string): PlanLimits;
105
+ /** True when `used + amount` fits under `limit`. `UNLIMITED` always fits. */
106
+ export declare function fitsWithin(limit: number, used: number, amount?: number): boolean;
107
+ /**
108
+ * Everything an admin UI needs to render a limits editor without hardcoding a
109
+ * single meter name — new keys appear in the form as soon as they exist here.
110
+ */
111
+ export declare const ENTITLEMENT_CATALOG: {
112
+ unlimited: number;
113
+ meters: readonly MeterDefinition[];
114
+ features: readonly FeatureDefinition[];
115
+ retentionDays: {
116
+ key: "retentionDays";
117
+ label: string;
118
+ unit: "days";
119
+ description: string;
120
+ };
121
+ defaults: Readonly<Record<string, PlanLimits>>;
122
+ fallbackPlan: string;
123
+ };
124
+ //# sourceMappingURL=definitions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/entitlements/definitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,gBAAgB,GAAG,OAAO,CAAC;AAE9E;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,CAAC;AAEvC,8FAA8F;AAC9F,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,OAAO,CAAC;AAE1C,2FAA2F;AAC3F,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,QAAQ,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,oGAAoG;IACpG,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,UAAU,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,iBAAiB,EAAE,SAAS,eAAe,EA6B9C,CAAC;AAEX,eAAO,MAAM,mBAAmB,EAAE,SAAS,iBAAiB,EAMlD,CAAC;AAEX,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAAsC,CAAC;AACjF,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAAwC,CAAC;AAMvF,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,WAAW,CAExD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,QAAQ,CAE3D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,UAAU,CAE/D;AAED,MAAM,WAAW,UAAU;IACzB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACtC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAID;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CA+BpE,CAAC;AAEF,iDAAiD;AACjD,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,eAAO,MAAM,aAAa,SAAS,CAAC;AAEpC;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAG3D;AAiBD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,iBAAiB,GAAG,IAAI,GAAG,UAAU,CAc1F;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,CAAC,EAAE,iBAAiB,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAE/F;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,OAAO,CAG3E;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;CAa/B,CAAC"}