@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
@@ -0,0 +1,159 @@
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.getUsage = getUsage;
7
+ exports.getUsageSnapshot = getUsageSnapshot;
8
+ exports.checkQuota = checkQuota;
9
+ exports.consumeQuota = consumeQuota;
10
+ exports.releaseQuota = releaseQuota;
11
+ exports.setUsage = setUsage;
12
+ const logger_1 = __importDefault(require("../logging/logger"));
13
+ const HttpError_1 = require("../errors/HttpError");
14
+ const UsageMeter_1 = require("./UsageMeter");
15
+ const definitions_1 = require("./definitions");
16
+ const mode_1 = require("./mode");
17
+ /** Current count for one meter in its live period. */
18
+ async function getUsage(subjectId, meter, now) {
19
+ const doc = await UsageMeter_1.UsageMeter.findOne({
20
+ subjectId: subjectId.toLowerCase(),
21
+ meter,
22
+ period: (0, UsageMeter_1.currentPeriod)(meter, now)
23
+ }).lean();
24
+ return doc?.count ?? 0;
25
+ }
26
+ /** Every meter at once, for the account page and the pricing table's live numbers. */
27
+ async function getUsageSnapshot(subjectId, now) {
28
+ const periods = definitions_1.METER_KEYS.map(meter => ({ meter, period: (0, UsageMeter_1.currentPeriod)(meter, now) }));
29
+ const docs = await UsageMeter_1.UsageMeter.find({
30
+ subjectId: subjectId.toLowerCase(),
31
+ $or: periods.map(p => ({ meter: p.meter, period: p.period }))
32
+ }).lean();
33
+ const snapshot = Object.fromEntries(definitions_1.METER_KEYS.map(k => [k, 0]));
34
+ for (const doc of docs) {
35
+ if (definitions_1.METER_KEYS.includes(doc.meter))
36
+ snapshot[doc.meter] = doc.count;
37
+ }
38
+ return snapshot;
39
+ }
40
+ function quotaError(req, used, limit) {
41
+ return new HttpError_1.PaymentRequiredError(`Limit reached for ${req.meter}`, {
42
+ code: 'QUOTA_EXCEEDED',
43
+ meter: req.meter,
44
+ used,
45
+ limit,
46
+ planCode: req.entitlements.planCode,
47
+ upgradeTo: req.entitlements.upgradeTo
48
+ });
49
+ }
50
+ function logWouldBlock(req, used, limit) {
51
+ logger_1.default.info('entitlement.would_block', {
52
+ event: 'entitlement.would_block',
53
+ meter: req.meter,
54
+ subjectId: req.subjectId,
55
+ amount: req.amount ?? 1,
56
+ used,
57
+ limit,
58
+ planCode: req.entitlements.planCode,
59
+ route: req.route,
60
+ stale: req.entitlements.stale === true
61
+ });
62
+ }
63
+ /**
64
+ * Read-only check for cumulative meters whose real increment happens later
65
+ * (storage is counted when the upload lands, not when it is authorized).
66
+ *
67
+ * Racy by construction — two concurrent requests can both pass — which is
68
+ * acceptable here precisely because the meters it guards are reconciled from
69
+ * the underlying data. Use `consumeQuota` where the count is the only record.
70
+ */
71
+ async function checkQuota(req) {
72
+ const mode = (0, mode_1.getEntitlementsMode)();
73
+ if (mode === 'off')
74
+ return;
75
+ const limit = req.entitlements.limits.meters[req.meter] ?? definitions_1.UNLIMITED;
76
+ if (limit === definitions_1.UNLIMITED)
77
+ return;
78
+ const used = await getUsage(req.subjectId, req.meter);
79
+ if ((0, definitions_1.fitsWithin)(limit, used, req.amount ?? 1))
80
+ return;
81
+ if (mode === 'observe') {
82
+ logWouldBlock(req, used, limit);
83
+ return;
84
+ }
85
+ throw quotaError(req, used, limit);
86
+ }
87
+ /**
88
+ * Atomically reserve capacity, throwing when the reservation would overrun.
89
+ *
90
+ * The increment and the check are one `$inc` + read, not a read-then-write:
91
+ * two concurrent requests that both read "24 used" against a limit of 25 would
92
+ * both proceed and overrun. Incrementing first and rejecting on the
93
+ * *post-increment* value means the loser of that race is the one rejected.
94
+ *
95
+ * A rejected reservation rolls its own increment back before throwing — unlike
96
+ * the provider-quota version this generalizes, where an over-limit count just
97
+ * sits there until the day rolls over. A per-user cumulative meter never rolls
98
+ * over, so a leaked increment is permanent and shows up months later as a user
99
+ * who cannot upload despite being well under their limit.
100
+ */
101
+ async function consumeQuota(req) {
102
+ const mode = (0, mode_1.getEntitlementsMode)();
103
+ if (mode === 'off')
104
+ return;
105
+ const amount = req.amount ?? 1;
106
+ const limit = req.entitlements.limits.meters[req.meter] ?? definitions_1.UNLIMITED;
107
+ const period = (0, UsageMeter_1.currentPeriod)(req.meter);
108
+ const subjectId = req.subjectId.toLowerCase();
109
+ const usage = await UsageMeter_1.UsageMeter.findOneAndUpdate({ subjectId, meter: req.meter, period }, { $inc: { count: amount } }, { upsert: true, returnDocument: 'after', setDefaultsOnInsert: true });
110
+ if (limit === definitions_1.UNLIMITED || usage.count <= limit)
111
+ return;
112
+ 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
+ }
119
+ await releaseQuota(subjectId, req.meter, amount);
120
+ throw quotaError(req, usedBefore, limit);
121
+ }
122
+ /**
123
+ * Hand back capacity after a failure that never consumed it (the upload errored,
124
+ * the AI service refused the job), and on deletes — a cumulative meter that only
125
+ * counts up is a bug that surfaces as an angry support email six months in.
126
+ *
127
+ * Never drops below zero, and never throws: a failed release must not turn a
128
+ * recoverable error into a 500 for the caller that was already unwinding.
129
+ */
130
+ async function releaseQuota(subjectId, meter, amount = 1, now) {
131
+ try {
132
+ const filter = { subjectId: subjectId.toLowerCase(), meter, period: (0, UsageMeter_1.currentPeriod)(meter, now) };
133
+ // Conditional `$inc` rather than read-then-write: a read-modify-write here
134
+ // loses concurrent releases, which on a cumulative meter is permanent drift.
135
+ const result = await UsageMeter_1.UsageMeter.updateOne({ ...filter, count: { $gte: amount } }, { $inc: { count: -amount } });
136
+ if (result.matchedCount === 0) {
137
+ // Either no counter exists, or it holds less than we are handing back
138
+ // (a reconciliation reset it in between). Clamp at zero, never negative.
139
+ await UsageMeter_1.UsageMeter.updateOne({ ...filter, count: { $lt: amount } }, { $set: { count: 0 } });
140
+ }
141
+ }
142
+ catch (err) {
143
+ logger_1.default.warn('Failed to release usage quota', {
144
+ subjectId,
145
+ meter,
146
+ amount,
147
+ error: err?.message
148
+ });
149
+ }
150
+ }
151
+ /**
152
+ * Set a meter to a known value. For the backfill (existing storage has to be
153
+ * seeded or every current user starts at zero and silently exceeds later) and
154
+ * for periodic reconciliation against the source of truth.
155
+ */
156
+ async function setUsage(subjectId, meter, count, now) {
157
+ await UsageMeter_1.UsageMeter.updateOne({ subjectId: subjectId.toLowerCase(), meter, period: (0, UsageMeter_1.currentPeriod)(meter, now) }, { $set: { count: Math.max(0, Math.floor(count)) } }, { upsert: true, setDefaultsOnInsert: true });
158
+ }
159
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/entitlements/usage.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,13 @@
9
9
  */
10
10
  export declare class HttpError extends Error {
11
11
  statusCode: number;
12
- constructor(statusCode: number, message: string);
12
+ /**
13
+ * Extra fields merged into the JSON body by the shared errorHandler. Used by
14
+ * errors the client has to *act* on rather than just display — a 402 tells
15
+ * the upgrade dialog which meter blocked and which plan lifts it.
16
+ */
17
+ details?: Record<string, unknown>;
18
+ constructor(statusCode: number, message: string, details?: Record<string, unknown>);
13
19
  }
14
20
  export declare class BadRequestError extends HttpError {
15
21
  constructor(message?: string);
@@ -29,4 +35,27 @@ export declare class ConflictError extends HttpError {
29
35
  export declare class TooManyRequestsError extends HttpError {
30
36
  constructor(message?: string);
31
37
  }
38
+ /**
39
+ * Fields the upgrade dialog renders. Every one of them comes from the response
40
+ * body, so the dialog needs no knowledge of which call failed.
41
+ */
42
+ export interface PaymentRequiredDetails {
43
+ /** Machine-readable reason, e.g. `QUOTA_EXCEEDED` or `FEATURE_NOT_IN_PLAN`. */
44
+ code: string;
45
+ meter?: string;
46
+ feature?: string;
47
+ used?: number;
48
+ limit?: number;
49
+ planCode?: string;
50
+ /** The cheapest plan that lifts this particular limit, when one exists. */
51
+ upgradeTo?: string;
52
+ }
53
+ /**
54
+ * 402, never 403. 403 means "you may not"; 402 means "you may, for money" —
55
+ * and the frontend needs to tell those apart to choose between an error toast
56
+ * and an upgrade dialog.
57
+ */
58
+ export declare class PaymentRequiredError extends HttpError {
59
+ constructor(message: string, details: PaymentRequiredDetails);
60
+ }
32
61
  //# sourceMappingURL=HttpError.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"HttpError.d.ts","sourceRoot":"","sources":["../../src/errors/HttpError.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,UAAU,EAAE,MAAM,CAAC;gBAEP,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAQhD;AAED,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,SAAgB;CAGpC;AAED,qBAAa,iBAAkB,SAAQ,SAAS;gBAClC,OAAO,SAAiB;CAGrC;AAED,qBAAa,cAAe,SAAQ,SAAS;gBAC/B,OAAO,SAAkB;CAGtC;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAAc;CAGlC;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAAa;CAGjC;AAED,qBAAa,oBAAqB,SAAQ,SAAS;gBACrC,OAAO,SAAsB;CAG1C"}
1
+ {"version":3,"file":"HttpError.d.ts","sourceRoot":"","sources":["../../src/errors/HttpError.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAEtB,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASnF;AAED,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,SAAgB;CAGpC;AAED,qBAAa,iBAAkB,SAAQ,SAAS;gBAClC,OAAO,SAAiB;CAGrC;AAED,qBAAa,cAAe,SAAQ,SAAS;gBAC/B,OAAO,SAAkB;CAGtC;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAAc;CAGlC;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAAa;CAGjC;AAED,qBAAa,oBAAqB,SAAQ,SAAS;gBACrC,OAAO,SAAsB;CAG1C;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,SAAS;gBACrC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB;CAG7D"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = void 0;
3
+ exports.PaymentRequiredError = exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = void 0;
4
4
  /**
5
5
  * Base class for errors that should be turned into a specific HTTP response
6
6
  * by the shared errorHandler, instead of every controller hand-rolling
@@ -12,10 +12,17 @@ exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = e
12
12
  */
13
13
  class HttpError extends Error {
14
14
  statusCode;
15
- constructor(statusCode, message) {
15
+ /**
16
+ * Extra fields merged into the JSON body by the shared errorHandler. Used by
17
+ * errors the client has to *act* on rather than just display — a 402 tells
18
+ * the upgrade dialog which meter blocked and which plan lifts it.
19
+ */
20
+ details;
21
+ constructor(statusCode, message, details) {
16
22
  super(message);
17
23
  this.name = new.target.name;
18
24
  this.statusCode = statusCode;
25
+ this.details = details;
19
26
  // Restore the prototype chain (needed when compiling to ES2018 targets
20
27
  // where `extends Error` doesn't preserve instanceof checks correctly).
21
28
  Object.setPrototypeOf(this, new.target.prototype);
@@ -58,4 +65,15 @@ class TooManyRequestsError extends HttpError {
58
65
  }
59
66
  }
60
67
  exports.TooManyRequestsError = TooManyRequestsError;
68
+ /**
69
+ * 402, never 403. 403 means "you may not"; 402 means "you may, for money" —
70
+ * and the frontend needs to tell those apart to choose between an error toast
71
+ * and an upgrade dialog.
72
+ */
73
+ class PaymentRequiredError extends HttpError {
74
+ constructor(message, details) {
75
+ super(402, message, { ...details });
76
+ }
77
+ }
78
+ exports.PaymentRequiredError = PaymentRequiredError;
61
79
  //# sourceMappingURL=HttpError.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"HttpError.js","sourceRoot":"","sources":["../../src/errors/HttpError.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,MAAa,SAAU,SAAQ,KAAK;IAClC,UAAU,CAAS;IAEnB,YAAY,UAAkB,EAAE,OAAe;QAC7C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,uEAAuE;QACvE,uEAAuE;QACvE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAXD,8BAWC;AAED,MAAa,eAAgB,SAAQ,SAAS;IAC5C,YAAY,OAAO,GAAG,aAAa;QACjC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,0CAIC;AAED,MAAa,iBAAkB,SAAQ,SAAS;IAC9C,YAAY,OAAO,GAAG,cAAc;QAClC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,8CAIC;AAED,MAAa,cAAe,SAAQ,SAAS;IAC3C,YAAY,OAAO,GAAG,eAAe;QACnC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,wCAIC;AAED,MAAa,aAAc,SAAQ,SAAS;IAC1C,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,aAAc,SAAQ,SAAS;IAC1C,YAAY,OAAO,GAAG,UAAU;QAC9B,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,oBAAqB,SAAQ,SAAS;IACjD,YAAY,OAAO,GAAG,mBAAmB;QACvC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,oDAIC"}
1
+ {"version":3,"file":"HttpError.js","sourceRoot":"","sources":["../../src/errors/HttpError.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,MAAa,SAAU,SAAQ,KAAK;IAClC,UAAU,CAAS;IACnB;;;;OAIG;IACH,OAAO,CAA2B;IAElC,YAAY,UAAkB,EAAE,OAAe,EAAE,OAAiC;QAChF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,uEAAuE;QACvE,uEAAuE;QACvE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAlBD,8BAkBC;AAED,MAAa,eAAgB,SAAQ,SAAS;IAC5C,YAAY,OAAO,GAAG,aAAa;QACjC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,0CAIC;AAED,MAAa,iBAAkB,SAAQ,SAAS;IAC9C,YAAY,OAAO,GAAG,cAAc;QAClC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,8CAIC;AAED,MAAa,cAAe,SAAQ,SAAS;IAC3C,YAAY,OAAO,GAAG,eAAe;QACnC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,wCAIC;AAED,MAAa,aAAc,SAAQ,SAAS;IAC1C,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,aAAc,SAAQ,SAAS;IAC1C,YAAY,OAAO,GAAG,UAAU;QAC9B,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,oBAAqB,SAAQ,SAAS;IACjD,YAAY,OAAO,GAAG,mBAAmB;QACvC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtB,CAAC;CACF;AAJD,oDAIC;AAkBD;;;;GAIG;AACH,MAAa,oBAAqB,SAAQ,SAAS;IACjD,YAAY,OAAe,EAAE,OAA+B;QAC1D,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IACtC,CAAC;CACF;AAJD,oDAIC"}
package/dist/index.d.ts CHANGED
@@ -8,8 +8,11 @@ export { createHealthCheck } from './health/createHealthCheck';
8
8
  export type { HealthCheckConfig } from './health/createHealthCheck';
9
9
  export { connectDB, disconnectDB } from './database/connection';
10
10
  export { requireEnv } from './config/env';
11
- export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, TooManyRequestsError } from './errors/HttpError';
11
+ export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, TooManyRequestsError, PaymentRequiredError } from './errors/HttpError';
12
+ export type { PaymentRequiredDetails } from './errors/HttpError';
13
+ export * from './entitlements';
12
14
  export { authenticateToken } from './middleware/authMiddleware';
15
+ export { requireInternalServiceToken, internalServiceTokenBypass, allowUserOrInternalService, verifyInternalToken, INTERNAL_TOKEN_HEADER, SERVICE_ID_HEADER } from './middleware/internalServiceAuth';
13
16
  export { createCorsMiddleware } from './middleware/corsMiddleware';
14
17
  export { errorHandler } from './middleware/errorHandler';
15
18
  export { requestLogger, requestLoggerWithMetrics, simpleRequestLogger } from './middleware/requestLogger';
@@ -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,MAAM,oBAAoB,CAAC;AAGvJ,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,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,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
@@ -1,11 +1,25 @@
1
1
  "use strict";
2
2
  // @tumbaland/backend-core - Core shared functionality for Tumbaland backend services
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
16
+ };
3
17
  var __importDefault = (this && this.__importDefault) || function (mod) {
4
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
19
  };
6
20
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.injectHeaders = exports.setSpanTag = exports.logToSpan = exports.createChildSpan = exports.tracingMiddleware = exports.startSpan = exports.getTracer = exports.initTracer = exports.register = exports.businessMetrics = exports.databaseQueriesTotal = exports.databaseQueryDuration = exports.httpRequestsTotal = exports.httpRequestDuration = exports.metricsMiddleware = exports.canAccessResource = exports.buildAccessQuery = exports.sendError = exports.sendSuccess = exports.createApiResponse = exports.correlationMiddleware = exports.generateCorrelationId = exports.validate = exports.strictRateLimiter = exports.standardRateLimiter = exports.createRateLimiter = exports.securityHeaders = exports.simpleRequestLogger = exports.requestLoggerWithMetrics = exports.requestLogger = exports.errorHandler = exports.createCorsMiddleware = exports.authenticateToken = exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = exports.requireEnv = exports.disconnectDB = exports.connectDB = exports.createHealthCheck = exports.metricsHandler = exports.healthLive = exports.healthCheck = exports.logger = exports.registerShutdown = exports.createBaseApp = void 0;
8
- exports.extractSpanContext = void 0;
21
+ exports.initTracer = exports.register = exports.businessMetrics = exports.databaseQueriesTotal = exports.databaseQueryDuration = exports.httpRequestsTotal = exports.httpRequestDuration = exports.metricsMiddleware = exports.canAccessResource = exports.buildAccessQuery = exports.sendError = exports.sendSuccess = exports.createApiResponse = exports.correlationMiddleware = exports.generateCorrelationId = exports.validate = exports.strictRateLimiter = exports.standardRateLimiter = exports.createRateLimiter = exports.securityHeaders = exports.simpleRequestLogger = exports.requestLoggerWithMetrics = exports.requestLogger = exports.errorHandler = exports.createCorsMiddleware = exports.SERVICE_ID_HEADER = exports.INTERNAL_TOKEN_HEADER = exports.verifyInternalToken = exports.allowUserOrInternalService = exports.internalServiceTokenBypass = exports.requireInternalServiceToken = exports.authenticateToken = exports.PaymentRequiredError = exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.HttpError = exports.requireEnv = exports.disconnectDB = exports.connectDB = exports.createHealthCheck = exports.metricsHandler = exports.healthLive = exports.healthCheck = exports.logger = exports.registerShutdown = exports.createBaseApp = void 0;
22
+ exports.extractSpanContext = exports.injectHeaders = exports.setSpanTag = exports.logToSpan = exports.createChildSpan = exports.tracingMiddleware = exports.startSpan = exports.getTracer = void 0;
9
23
  // App bootstrap
10
24
  var createBaseApp_1 = require("./app/createBaseApp");
11
25
  Object.defineProperty(exports, "createBaseApp", { enumerable: true, get: function () { return createBaseApp_1.createBaseApp; } });
@@ -37,9 +51,19 @@ Object.defineProperty(exports, "ForbiddenError", { enumerable: true, get: functi
37
51
  Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return HttpError_1.NotFoundError; } });
38
52
  Object.defineProperty(exports, "ConflictError", { enumerable: true, get: function () { return HttpError_1.ConflictError; } });
39
53
  Object.defineProperty(exports, "TooManyRequestsError", { enumerable: true, get: function () { return HttpError_1.TooManyRequestsError; } });
54
+ Object.defineProperty(exports, "PaymentRequiredError", { enumerable: true, get: function () { return HttpError_1.PaymentRequiredError; } });
55
+ // Entitlements (plan limits, usage meters, enforcement)
56
+ __exportStar(require("./entitlements"), exports);
40
57
  // Middleware
41
58
  var authMiddleware_1 = require("./middleware/authMiddleware");
42
59
  Object.defineProperty(exports, "authenticateToken", { enumerable: true, get: function () { return authMiddleware_1.authenticateToken; } });
60
+ var internalServiceAuth_1 = require("./middleware/internalServiceAuth");
61
+ Object.defineProperty(exports, "requireInternalServiceToken", { enumerable: true, get: function () { return internalServiceAuth_1.requireInternalServiceToken; } });
62
+ Object.defineProperty(exports, "internalServiceTokenBypass", { enumerable: true, get: function () { return internalServiceAuth_1.internalServiceTokenBypass; } });
63
+ Object.defineProperty(exports, "allowUserOrInternalService", { enumerable: true, get: function () { return internalServiceAuth_1.allowUserOrInternalService; } });
64
+ Object.defineProperty(exports, "verifyInternalToken", { enumerable: true, get: function () { return internalServiceAuth_1.verifyInternalToken; } });
65
+ Object.defineProperty(exports, "INTERNAL_TOKEN_HEADER", { enumerable: true, get: function () { return internalServiceAuth_1.INTERNAL_TOKEN_HEADER; } });
66
+ Object.defineProperty(exports, "SERVICE_ID_HEADER", { enumerable: true, get: function () { return internalServiceAuth_1.SERVICE_ID_HEADER; } });
43
67
  var corsMiddleware_1 = require("./middleware/corsMiddleware");
44
68
  Object.defineProperty(exports, "createCorsMiddleware", { enumerable: true, get: function () { return corsMiddleware_1.createCorsMiddleware; } });
45
69
  var errorHandler_1 = require("./middleware/errorHandler");
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,gDAAuJ;AAA9I,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;AAE1H,aAAa;AACb,8DAAgE;AAAvD,mHAAA,iBAAiB,OAAA;AAC1B,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,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 +1 @@
1
- {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA4B/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,EAAE,mBA0D1B,CAAC"}
1
+ {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA8B/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,EAAE,mBA6D1B,CAAC"}
@@ -16,7 +16,7 @@ const HttpError_1 = require("../errors/HttpError");
16
16
  */
17
17
  function classifyError(error) {
18
18
  if (error instanceof HttpError_1.HttpError) {
19
- return { statusCode: error.statusCode, name: error.name, message: error.message };
19
+ return { statusCode: error.statusCode, name: error.name, message: error.message, details: error.details };
20
20
  }
21
21
  if (error.name === 'ValidationError' || error.name === 'CastError') {
22
22
  return { statusCode: 400, name: error.name, message: error.message };
@@ -68,6 +68,9 @@ const errorHandler = (error, req, res, _next) => {
68
68
  }
69
69
  if (known) {
70
70
  res.status(known.statusCode).json({
71
+ // Spread first so a stray details key can't shadow the envelope fields
72
+ // every client already relies on.
73
+ ...known.details,
71
74
  success: false,
72
75
  error: known.name,
73
76
  message: known.message
@@ -1 +1 @@
1
- {"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":";;;;;;AACA,+DAAuC;AACvC,mDAAgD;AAQhD;;;;;;;GAOG;AACH,SAAS,aAAa,CAAC,KAAY;IACjC,IAAI,KAAK,YAAY,qBAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IACpF,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACnE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IACvE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACI,MAAM,YAAY,GAAwB,CAC/C,KAAY,EACZ,GAAY,EACZ,GAAa,EACb,KAAmB,EACb,EAAE;IACR,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAEnC,IAAI,KAAK,EAAE,CAAC;QACV,uEAAuE;QACvE,kEAAkE;QAClE,gBAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE;YAC7B,KAAK,EAAE,KAAK,CAAC,OAAO;YACpB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,aAAa,EAAE,GAAG,CAAC,aAAa;SACjC,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,gBAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE;YAC/B,KAAK,EAAE,KAAK,CAAC,OAAO;YACpB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;YAChC,aAAa,EAAE,GAAG,CAAC,aAAa;SACjC,CAAC,CAAC;IACL,CAAC;IAED,sCAAsC;IACtC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC;IAClC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC;QAChF,GAAG,CAAC,MAAM,CAAC,6BAA6B,EAAE,MAAM,CAAC,CAAC;QAClD,GAAG,CAAC,MAAM,CAAC,kCAAkC,EAAE,MAAM,CAAC,CAAC;QACvD,GAAG,CAAC,MAAM,CAAC,8BAA8B,EAAE,iCAAiC,CAAC,CAAC;QAC9E,GAAG,CAAC,MAAM,CAAC,8BAA8B,EAAE,+EAA+E,CAAC,CAAC;IAC9H,CAAC;IAED,IAAI,KAAK,EAAE,CAAC;QACV,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YAChC,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,KAAK,CAAC,IAAI;YACjB,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,+DAA+D;IAC/D,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,CAAC;IAC7D,MAAM,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;IAE5E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,uBAAuB;QAC9B,OAAO,EAAE,YAAY;QACrB,GAAG,CAAC,aAAa,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;KAC7C,CAAC,CAAC;AACL,CAAC,CAAC;AA1DW,QAAA,YAAY,gBA0DvB"}
1
+ {"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { RequestHandler } from 'express';
2
+ /**
3
+ * Fields set by the internal-service middlewares below. Declared globally so
4
+ * routers and controllers read `req.isInternalService` off the same `Request`
5
+ * type Express itself uses, with no cast at the call site.
6
+ */
7
+ declare global {
8
+ namespace Express {
9
+ interface Request {
10
+ isInternalService?: boolean;
11
+ serviceIdentifier?: string;
12
+ }
13
+ }
14
+ }
15
+ export declare const INTERNAL_TOKEN_HEADER = "x-internal-token";
16
+ export declare const SERVICE_ID_HEADER = "x-service-id";
17
+ /**
18
+ * Constant-time comparison of the presented token against the configured one.
19
+ *
20
+ * `===` on secrets leaks their prefix through timing, and `timingSafeEqual`
21
+ * throws on a length mismatch (which is itself a leak), so length is compared
22
+ * first and both buffers are hashed to a fixed width — a token of the wrong
23
+ * length takes the same path as one of the right length with wrong bytes.
24
+ */
25
+ export declare function verifyInternalToken(presented: unknown): boolean;
26
+ /**
27
+ * Hard gate for endpoints that are only ever called service-to-service.
28
+ * No user-JWT fallback: a missing or wrong token is always a 401.
29
+ */
30
+ export declare const requireInternalServiceToken: RequestHandler;
31
+ /**
32
+ * For routers that serve both browsers and services: when the internal-token
33
+ * header is present it is *verified here*, and a bad token is rejected rather
34
+ * than passed along.
35
+ *
36
+ * The bug this replaces: treating the mere presence of the header as grounds to
37
+ * skip JWT verification, leaving the real check to a second middleware mounted
38
+ * per-router — so any route that forgot to mount the second one was reachable
39
+ * by sending `x-internal-token: anything`.
40
+ */
41
+ export declare const internalServiceTokenBypass: RequestHandler;
42
+ /** Allows a request that authenticated as either a user or an internal service. */
43
+ export declare const allowUserOrInternalService: RequestHandler;
44
+ //# sourceMappingURL=internalServiceAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internalServiceAuth.d.ts","sourceRoot":"","sources":["../../src/middleware/internalServiceAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,cAAc,EAAE,MAAM,SAAS,CAAC;AAIlD;;;;GAIG;AACH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,iBAAiB,CAAC,EAAE,OAAO,CAAC;YAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;SAC5B;KACF;CACF;AAED,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AACxD,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAS/D;AAOD;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,cAkBzC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,EAAE,cAOxC,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,EAAE,cAMxC,CAAC"}
@@ -0,0 +1,86 @@
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.allowUserOrInternalService = exports.internalServiceTokenBypass = exports.requireInternalServiceToken = exports.SERVICE_ID_HEADER = exports.INTERNAL_TOKEN_HEADER = void 0;
7
+ exports.verifyInternalToken = verifyInternalToken;
8
+ const crypto_1 = require("crypto");
9
+ const logger_1 = __importDefault(require("../logging/logger"));
10
+ exports.INTERNAL_TOKEN_HEADER = 'x-internal-token';
11
+ exports.SERVICE_ID_HEADER = 'x-service-id';
12
+ /**
13
+ * Constant-time comparison of the presented token against the configured one.
14
+ *
15
+ * `===` on secrets leaks their prefix through timing, and `timingSafeEqual`
16
+ * throws on a length mismatch (which is itself a leak), so length is compared
17
+ * first and both buffers are hashed to a fixed width — a token of the wrong
18
+ * length takes the same path as one of the right length with wrong bytes.
19
+ */
20
+ function verifyInternalToken(presented) {
21
+ const expected = process.env.INTERNAL_SERVICE_TOKEN;
22
+ if (!expected)
23
+ return false;
24
+ if (typeof presented !== 'string' || presented.length === 0)
25
+ return false;
26
+ const a = Buffer.from(presented, 'utf8');
27
+ const b = Buffer.from(expected, 'utf8');
28
+ if (a.length !== b.length)
29
+ return false;
30
+ return (0, crypto_1.timingSafeEqual)(a, b);
31
+ }
32
+ function markInternal(req) {
33
+ req.isInternalService = true;
34
+ req.serviceIdentifier = req.headers[exports.SERVICE_ID_HEADER] || 'unknown';
35
+ }
36
+ /**
37
+ * Hard gate for endpoints that are only ever called service-to-service.
38
+ * No user-JWT fallback: a missing or wrong token is always a 401.
39
+ */
40
+ const requireInternalServiceToken = (req, res, next) => {
41
+ if (!process.env.INTERNAL_SERVICE_TOKEN) {
42
+ logger_1.default.error('INTERNAL_SERVICE_TOKEN not configured');
43
+ res.status(500).json({ success: false, message: 'Internal service authentication not configured' });
44
+ return;
45
+ }
46
+ if (!verifyInternalToken(req.headers[exports.INTERNAL_TOKEN_HEADER])) {
47
+ logger_1.default.warn('Rejected internal service request', {
48
+ serviceId: req.headers[exports.SERVICE_ID_HEADER] || 'unknown',
49
+ path: req.originalUrl
50
+ });
51
+ res.status(401).json({ success: false, message: 'Invalid or missing internal service token' });
52
+ return;
53
+ }
54
+ markInternal(req);
55
+ next();
56
+ };
57
+ exports.requireInternalServiceToken = requireInternalServiceToken;
58
+ /**
59
+ * For routers that serve both browsers and services: when the internal-token
60
+ * header is present it is *verified here*, and a bad token is rejected rather
61
+ * than passed along.
62
+ *
63
+ * The bug this replaces: treating the mere presence of the header as grounds to
64
+ * skip JWT verification, leaving the real check to a second middleware mounted
65
+ * per-router — so any route that forgot to mount the second one was reachable
66
+ * by sending `x-internal-token: anything`.
67
+ */
68
+ const internalServiceTokenBypass = (req, res, next) => {
69
+ const presented = req.headers[exports.INTERNAL_TOKEN_HEADER];
70
+ if (presented === undefined) {
71
+ next();
72
+ return;
73
+ }
74
+ (0, exports.requireInternalServiceToken)(req, res, next);
75
+ };
76
+ exports.internalServiceTokenBypass = internalServiceTokenBypass;
77
+ /** Allows a request that authenticated as either a user or an internal service. */
78
+ const allowUserOrInternalService = (req, res, next) => {
79
+ if (req.isInternalService || req.user) {
80
+ next();
81
+ return;
82
+ }
83
+ res.status(401).json({ success: false, message: 'Authentication required' });
84
+ };
85
+ exports.allowUserOrInternalService = allowUserOrInternalService;
86
+ //# sourceMappingURL=internalServiceAuth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internalServiceAuth.js","sourceRoot":"","sources":["../../src/middleware/internalServiceAuth.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tumbaland/backend-core",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "description": "Core shared functionality for Tumbaland backend services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,6 +36,7 @@
36
36
  "@types/morgan": "^1.9.10",
37
37
  "@types/node": "^26.1.0",
38
38
  "commit-and-tag-version": "^13.1.2",
39
+ "mongodb-memory-server": "^11.2.0",
39
40
  "typescript": "^6.0.3"
40
41
  },
41
42
  "dependencies": {
@@ -63,6 +63,37 @@ describe('createBaseApp', () => {
63
63
  expect(res.status).toBe(200);
64
64
  });
65
65
 
66
+ it('trusts the proxy, so X-Forwarded-For identifies the client rather than being ignored', async () => {
67
+ const app = createBaseApp();
68
+ app.get('/whoami', (req, res) => res.json({ ip: req.ip }));
69
+
70
+ const res = await request(app).get('/whoami').set('X-Forwarded-For', '203.0.113.7');
71
+
72
+ expect(res.body.ip).toBe('203.0.113.7');
73
+ });
74
+
75
+ it('keeps rate-limit buckets per client, not one shared bucket behind the proxy', async () => {
76
+ const app = createBaseApp({ rateLimiter: (await import('../middleware/security')).createRateLimiter({ windowMs: 60_000, max: 1 }) });
77
+ app.get('/ping', (_req, res) => res.json({ ok: true }));
78
+
79
+ const clientA = await request(app).get('/ping').set('X-Forwarded-For', '203.0.113.7');
80
+ const clientB = await request(app).get('/ping').set('X-Forwarded-For', '198.51.100.4');
81
+ const clientAAgain = await request(app).get('/ping').set('X-Forwarded-For', '203.0.113.7');
82
+
83
+ expect(clientA.status).toBe(200);
84
+ expect(clientB.status).toBe(200); // a different client is unaffected by A exhausting its window
85
+ expect(clientAAgain.status).toBe(429);
86
+ });
87
+
88
+ it('ignores X-Forwarded-For when trustProxy is disabled', async () => {
89
+ const app = createBaseApp({ trustProxy: false });
90
+ app.get('/whoami', (req, res) => res.json({ ip: req.ip }));
91
+
92
+ const res = await request(app).get('/whoami').set('X-Forwarded-For', '203.0.113.7');
93
+
94
+ expect(res.body.ip).not.toBe('203.0.113.7');
95
+ });
96
+
66
97
  it('mounts /health/live ahead of the rate limiter, so it never gets 429s a busy service would', async () => {
67
98
  const app = createBaseApp({ rateLimiter: (await import('../middleware/security')).createRateLimiter({ windowMs: 60_000, max: 1 }) });
68
99
  app.get('/ping', (_req, res) => res.json({ ok: true }));