@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.
- package/dist/app/createBaseApp.d.ts +24 -0
- package/dist/app/createBaseApp.d.ts.map +1 -1
- package/dist/app/createBaseApp.js +20 -1
- package/dist/app/createBaseApp.js.map +1 -1
- package/dist/entitlements/UsageMeter.d.ts +30 -0
- package/dist/entitlements/UsageMeter.d.ts.map +1 -0
- package/dist/entitlements/UsageMeter.js +27 -0
- package/dist/entitlements/UsageMeter.js.map +1 -0
- package/dist/entitlements/client.d.ts +17 -0
- package/dist/entitlements/client.d.ts.map +1 -0
- package/dist/entitlements/client.js +142 -0
- package/dist/entitlements/client.js.map +1 -0
- package/dist/entitlements/definitions.d.ts +124 -0
- package/dist/entitlements/definitions.d.ts.map +1 -0
- package/dist/entitlements/definitions.js +210 -0
- package/dist/entitlements/definitions.js.map +1 -0
- package/dist/entitlements/index.d.ts +12 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +43 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/entitlements/middleware.d.ts +54 -0
- package/dist/entitlements/middleware.d.ts.map +1 -0
- package/dist/entitlements/middleware.js +109 -0
- package/dist/entitlements/middleware.js.map +1 -0
- package/dist/entitlements/mode.d.ts +10 -0
- package/dist/entitlements/mode.d.ts.map +1 -0
- package/dist/entitlements/mode.js +32 -0
- package/dist/entitlements/mode.js.map +1 -0
- package/dist/entitlements/types.d.ts +29 -0
- package/dist/entitlements/types.d.ts.map +1 -0
- package/dist/entitlements/types.js +3 -0
- package/dist/entitlements/types.js.map +1 -0
- package/dist/entitlements/usage.d.ts +56 -0
- package/dist/entitlements/usage.d.ts.map +1 -0
- package/dist/entitlements/usage.js +159 -0
- package/dist/entitlements/usage.js.map +1 -0
- package/dist/errors/HttpError.d.ts +30 -1
- package/dist/errors/HttpError.d.ts.map +1 -1
- package/dist/errors/HttpError.js +20 -2
- package/dist/errors/HttpError.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/errorHandler.d.ts.map +1 -1
- package/dist/middleware/errorHandler.js +4 -1
- package/dist/middleware/errorHandler.js.map +1 -1
- package/dist/middleware/internalServiceAuth.d.ts +44 -0
- package/dist/middleware/internalServiceAuth.d.ts.map +1 -0
- package/dist/middleware/internalServiceAuth.js +86 -0
- package/dist/middleware/internalServiceAuth.js.map +1 -0
- package/package.json +2 -1
- package/src/app/createBaseApp.test.ts +31 -0
- package/src/app/createBaseApp.ts +28 -1
- package/src/entitlements/UsageMeter.ts +49 -0
- package/src/entitlements/client.test.ts +170 -0
- package/src/entitlements/client.ts +162 -0
- package/src/entitlements/definitions.test.ts +161 -0
- package/src/entitlements/definitions.ts +261 -0
- package/src/entitlements/index.ts +40 -0
- package/src/entitlements/middleware.test.ts +179 -0
- package/src/entitlements/middleware.ts +136 -0
- package/src/entitlements/mode.test.ts +50 -0
- package/src/entitlements/mode.ts +28 -0
- package/src/entitlements/types.ts +30 -0
- package/src/entitlements/usage.test.ts +271 -0
- package/src/entitlements/usage.ts +189 -0
- package/src/errors/HttpError.test.ts +39 -1
- package/src/errors/HttpError.ts +35 -1
- package/src/index.ts +13 -1
- package/src/middleware/errorHandler.test.ts +43 -1
- package/src/middleware/errorHandler.ts +6 -1
- package/src/middleware/internalServiceAuth.test.ts +173 -0
- 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
|
-
|
|
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;
|
|
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"}
|
package/dist/errors/HttpError.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,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.
|
|
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
|
|
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;
|
|
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;
|
|
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.
|
|
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 }));
|