@adaptic/backend-legacy 0.0.997 → 0.0.999
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/esm/auth/authorization-map.d.ts +77 -0
- package/esm/auth/authorization-map.d.ts.map +1 -0
- package/esm/auth/authorization-map.js.map +1 -0
- package/esm/auth/authorization-map.mjs +132 -0
- package/esm/auth/cortex-auth-checker.d.ts +10 -0
- package/esm/auth/cortex-auth-checker.d.ts.map +1 -1
- package/esm/auth/cortex-auth-checker.js.map +1 -1
- package/esm/auth/cortex-auth-checker.mjs +17 -0
- package/esm/middleware/__tests__/rate-limit-test-utils.d.ts +60 -0
- package/esm/middleware/__tests__/rate-limit-test-utils.d.ts.map +1 -0
- package/esm/middleware/__tests__/rate-limit-test-utils.js.map +1 -0
- package/esm/middleware/__tests__/rate-limit-test-utils.mjs +85 -0
- package/esm/middleware/rate-limiter.d.ts +63 -9
- package/esm/middleware/rate-limiter.d.ts.map +1 -1
- package/esm/middleware/rate-limiter.js.map +1 -1
- package/esm/middleware/rate-limiter.mjs +108 -64
- package/esm/middleware/rate-tier.d.ts +56 -0
- package/esm/middleware/rate-tier.d.ts.map +1 -0
- package/esm/middleware/rate-tier.js.map +1 -0
- package/esm/middleware/rate-tier.mjs +103 -0
- package/esm/utils/logger.d.ts.map +1 -1
- package/esm/utils/logger.js.map +1 -1
- package/esm/utils/logger.mjs +35 -12
- package/package.json +1 -1
- package/server.cjs +57 -11
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORTEX-P0-001 phase 2 — `@Authorized()` coverage for generated CRUD resolvers
|
|
3
|
+
* (audit B01-backend-legacy-03).
|
|
4
|
+
*
|
|
5
|
+
* The `cortexAuthChecker` registered in `buildSchema` is only invoked by
|
|
6
|
+
* TypeGraphQL for fields carrying an `@Authorized()` decorator; the
|
|
7
|
+
* auto-generated typegraphql-prisma resolvers carry none, so the checker was
|
|
8
|
+
* registered-but-inert. This module applies decorators via
|
|
9
|
+
* `applyResolversEnhanceMap`, making the checker actually execute:
|
|
10
|
+
*
|
|
11
|
+
* - FULL coverage (`_all`) for the five investor-relations models —
|
|
12
|
+
* regulated PII (tax documents, KYC status, LP capital ledger) that must
|
|
13
|
+
* never sit on an unauthenticated CRUD surface.
|
|
14
|
+
* - DELETE mutations (`deleteOne*` / `deleteMany*`) for every other model —
|
|
15
|
+
* the most destructive operations on the trading system-of-record.
|
|
16
|
+
*
|
|
17
|
+
* CRITICAL SAFETY CONTRACT — the checker itself remains SHADOW-FIRST: while
|
|
18
|
+
* `CORTEX_AUTHCHECKER_ENFORCE` is OFF (default) a would-deny operation is
|
|
19
|
+
* logged + counted and then ALLOWED, so engine service traffic cannot break.
|
|
20
|
+
* This module only determines WHERE the checker runs, not what it does.
|
|
21
|
+
* Graduation rule (audit B01-backend-legacy-07): enforce only after
|
|
22
|
+
* `cortex_authchecker_evaluations_total` > 0 and the would-deny series has
|
|
23
|
+
* been observed over a full trading week.
|
|
24
|
+
*
|
|
25
|
+
* Must be applied BEFORE `buildSchema` — decorators land in TypeGraphQL's
|
|
26
|
+
* global metadata storage, which `buildSchema` reads.
|
|
27
|
+
*/
|
|
28
|
+
import { type ResolversEnhanceMap } from '../generated/typegraphql-prisma';
|
|
29
|
+
/** Model names accepted by the generated {@link ResolversEnhanceMap}. */
|
|
30
|
+
type EnhanceModelName = keyof ResolversEnhanceMap;
|
|
31
|
+
/**
|
|
32
|
+
* The five investor-relations models receiving FULL (`_all`) coverage:
|
|
33
|
+
* every generated query and mutation on these models invokes the authChecker.
|
|
34
|
+
*/
|
|
35
|
+
export declare const IR_MODELS: readonly EnhanceModelName[];
|
|
36
|
+
/** Summary of the coverage applied, for the boot log and tests. */
|
|
37
|
+
export interface AuthorizationMapSummary {
|
|
38
|
+
/** Models decorated with `_all` (full CRUD coverage). */
|
|
39
|
+
readonly fullCoverageModels: number;
|
|
40
|
+
/** Models whose delete mutations were decorated. */
|
|
41
|
+
readonly deleteCoverageModels: number;
|
|
42
|
+
/** Total decorated resolver actions (delete actions + `_all` markers). */
|
|
43
|
+
readonly decoratedActions: number;
|
|
44
|
+
/**
|
|
45
|
+
* Delete-action names skipped because the generated CRUD resolver does not
|
|
46
|
+
* expose them — non-empty output means the naming convention drifted and the
|
|
47
|
+
* map must be updated. Never silently ignored: logged at warn on apply.
|
|
48
|
+
*/
|
|
49
|
+
readonly skippedActions: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
/** Internal build output: the map plus its audit summary. */
|
|
52
|
+
interface BuiltAuthorizationMap {
|
|
53
|
+
readonly map: ResolversEnhanceMap;
|
|
54
|
+
readonly summary: AuthorizationMapSummary;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Builds the CORTEX authorization enhance-map. Pure (no metadata mutation) so
|
|
58
|
+
* tests can pin its contents without touching TypeGraphQL global state.
|
|
59
|
+
*
|
|
60
|
+
* @returns The enhance map and a summary of the coverage it encodes.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildCortexAuthorizationMap(): BuiltAuthorizationMap;
|
|
63
|
+
/**
|
|
64
|
+
* Applies the CORTEX authorization map to the generated resolvers. Idempotent:
|
|
65
|
+
* decorators land in TypeGraphQL's global metadata storage, so applying twice
|
|
66
|
+
* would duplicate `Authorized` metadata — subsequent calls return the cached
|
|
67
|
+
* summary without re-applying.
|
|
68
|
+
*
|
|
69
|
+
* Call BEFORE `buildSchema`. The registered `cortexAuthChecker` stays
|
|
70
|
+
* SHADOW-FIRST (log + count + allow) until `CORTEX_AUTHCHECKER_ENFORCE` is
|
|
71
|
+
* explicitly flipped on.
|
|
72
|
+
*
|
|
73
|
+
* @returns The {@link AuthorizationMapSummary} for the boot log.
|
|
74
|
+
*/
|
|
75
|
+
export declare function applyCortexAuthorizationMap(): AuthorizationMapSummary;
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=authorization-map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorization-map.d.ts","sourceRoot":"","sources":["../../../src/auth/authorization-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AAIzC,yEAAyE;AACzE,KAAK,gBAAgB,GAAG,MAAM,mBAAmB,CAAC;AAElD;;;GAGG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,gBAAgB,EAMhD,CAAC;AAEF,mEAAmE;AACnE,MAAM,WAAW,uBAAuB;IACtC,yDAAyD;IACzD,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,oDAAoD;IACpD,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,0EAA0E;IAC1E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C;AAmBD,6DAA6D;AAC7D,UAAU,qBAAqB;IAC7B,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAC;CAC3C;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,qBAAqB,CA8CnE;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,IAAI,uBAAuB,CAcrE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorization-map.js","sourceRoot":"","sources":["../../../src/auth/authorization-map.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORTEX-P0-001 phase 2 — `@Authorized()` coverage for generated CRUD resolvers
|
|
3
|
+
* (audit B01-backend-legacy-03).
|
|
4
|
+
*
|
|
5
|
+
* The `cortexAuthChecker` registered in `buildSchema` is only invoked by
|
|
6
|
+
* TypeGraphQL for fields carrying an `@Authorized()` decorator; the
|
|
7
|
+
* auto-generated typegraphql-prisma resolvers carry none, so the checker was
|
|
8
|
+
* registered-but-inert. This module applies decorators via
|
|
9
|
+
* `applyResolversEnhanceMap`, making the checker actually execute:
|
|
10
|
+
*
|
|
11
|
+
* - FULL coverage (`_all`) for the five investor-relations models —
|
|
12
|
+
* regulated PII (tax documents, KYC status, LP capital ledger) that must
|
|
13
|
+
* never sit on an unauthenticated CRUD surface.
|
|
14
|
+
* - DELETE mutations (`deleteOne*` / `deleteMany*`) for every other model —
|
|
15
|
+
* the most destructive operations on the trading system-of-record.
|
|
16
|
+
*
|
|
17
|
+
* CRITICAL SAFETY CONTRACT — the checker itself remains SHADOW-FIRST: while
|
|
18
|
+
* `CORTEX_AUTHCHECKER_ENFORCE` is OFF (default) a would-deny operation is
|
|
19
|
+
* logged + counted and then ALLOWED, so engine service traffic cannot break.
|
|
20
|
+
* This module only determines WHERE the checker runs, not what it does.
|
|
21
|
+
* Graduation rule (audit B01-backend-legacy-07): enforce only after
|
|
22
|
+
* `cortex_authchecker_evaluations_total` > 0 and the would-deny series has
|
|
23
|
+
* been observed over a full trading week.
|
|
24
|
+
*
|
|
25
|
+
* Must be applied BEFORE `buildSchema` — decorators land in TypeGraphQL's
|
|
26
|
+
* global metadata storage, which `buildSchema` reads.
|
|
27
|
+
*/
|
|
28
|
+
import { Authorized } from 'type-graphql';
|
|
29
|
+
import { Prisma } from '@prisma/client';
|
|
30
|
+
import { applyResolversEnhanceMap, } from '../generated/typegraphql-prisma/index.mjs';
|
|
31
|
+
import * as generatedIndex from '../generated/typegraphql-prisma/index.mjs';
|
|
32
|
+
import { logger } from '../utils/logger.mjs';
|
|
33
|
+
/**
|
|
34
|
+
* The five investor-relations models receiving FULL (`_all`) coverage:
|
|
35
|
+
* every generated query and mutation on these models invokes the authChecker.
|
|
36
|
+
*/
|
|
37
|
+
export const IR_MODELS = [
|
|
38
|
+
'Investor',
|
|
39
|
+
'Holding',
|
|
40
|
+
'InvestorTransaction',
|
|
41
|
+
'InvestorDocument',
|
|
42
|
+
'TaxDocument',
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* Looks up the generated `<Model>CrudResolver` class for a model, so delete
|
|
46
|
+
* action names can be validated against the real resolver surface instead of
|
|
47
|
+
* being assumed by convention.
|
|
48
|
+
*/
|
|
49
|
+
function crudResolverPrototype(model) {
|
|
50
|
+
const resolverClass = generatedIndex[`${model}CrudResolver`];
|
|
51
|
+
if (typeof resolverClass !== 'function') {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return resolverClass.prototype;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Builds the CORTEX authorization enhance-map. Pure (no metadata mutation) so
|
|
58
|
+
* tests can pin its contents without touching TypeGraphQL global state.
|
|
59
|
+
*
|
|
60
|
+
* @returns The enhance map and a summary of the coverage it encodes.
|
|
61
|
+
*/
|
|
62
|
+
export function buildCortexAuthorizationMap() {
|
|
63
|
+
const map = {};
|
|
64
|
+
const skippedActions = [];
|
|
65
|
+
let decoratedActions = 0;
|
|
66
|
+
let deleteCoverageModels = 0;
|
|
67
|
+
for (const model of IR_MODELS) {
|
|
68
|
+
map[model] = { _all: [Authorized()] };
|
|
69
|
+
decoratedActions += 1;
|
|
70
|
+
}
|
|
71
|
+
const irModelSet = new Set(IR_MODELS);
|
|
72
|
+
for (const model of Object.values(Prisma.ModelName)) {
|
|
73
|
+
// IR models already have full coverage; decorating deletes again would
|
|
74
|
+
// register duplicate Authorized metadata on the same fields.
|
|
75
|
+
if (irModelSet.has(model)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const prototype = crudResolverPrototype(model);
|
|
79
|
+
const actionsConfig = {};
|
|
80
|
+
let coveredForModel = 0;
|
|
81
|
+
for (const action of [`deleteOne${model}`, `deleteMany${model}`]) {
|
|
82
|
+
if (prototype && typeof prototype[action] === 'function') {
|
|
83
|
+
actionsConfig[action] = [Authorized()];
|
|
84
|
+
coveredForModel += 1;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
skippedActions.push(action);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (coveredForModel > 0) {
|
|
91
|
+
map[model] =
|
|
92
|
+
actionsConfig;
|
|
93
|
+
decoratedActions += coveredForModel;
|
|
94
|
+
deleteCoverageModels += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
map,
|
|
99
|
+
summary: {
|
|
100
|
+
fullCoverageModels: IR_MODELS.length,
|
|
101
|
+
deleteCoverageModels,
|
|
102
|
+
decoratedActions,
|
|
103
|
+
skippedActions,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let appliedSummary;
|
|
108
|
+
/**
|
|
109
|
+
* Applies the CORTEX authorization map to the generated resolvers. Idempotent:
|
|
110
|
+
* decorators land in TypeGraphQL's global metadata storage, so applying twice
|
|
111
|
+
* would duplicate `Authorized` metadata — subsequent calls return the cached
|
|
112
|
+
* summary without re-applying.
|
|
113
|
+
*
|
|
114
|
+
* Call BEFORE `buildSchema`. The registered `cortexAuthChecker` stays
|
|
115
|
+
* SHADOW-FIRST (log + count + allow) until `CORTEX_AUTHCHECKER_ENFORCE` is
|
|
116
|
+
* explicitly flipped on.
|
|
117
|
+
*
|
|
118
|
+
* @returns The {@link AuthorizationMapSummary} for the boot log.
|
|
119
|
+
*/
|
|
120
|
+
export function applyCortexAuthorizationMap() {
|
|
121
|
+
if (appliedSummary) {
|
|
122
|
+
return appliedSummary;
|
|
123
|
+
}
|
|
124
|
+
const { map, summary } = buildCortexAuthorizationMap();
|
|
125
|
+
applyResolversEnhanceMap(map);
|
|
126
|
+
if (summary.skippedActions.length > 0) {
|
|
127
|
+
logger.warn('[cortex-authz] delete actions missing from generated resolvers — authorization map drifted from codegen', { skippedActions: summary.skippedActions });
|
|
128
|
+
}
|
|
129
|
+
appliedSummary = summary;
|
|
130
|
+
return summary;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=authorization-map.js.map
|
|
@@ -57,6 +57,16 @@ export declare function isAuthCheckerEnforced(env?: Record<string, string | unde
|
|
|
57
57
|
* label on {@link cortexAuthCheckerWouldDenyTotal}.
|
|
58
58
|
*/
|
|
59
59
|
export type WouldDenyReason = 'unauthenticated' | 'insufficient_role';
|
|
60
|
+
/**
|
|
61
|
+
* Counts EVERY AuthChecker invocation, regardless of outcome (audit
|
|
62
|
+
* B01-backend-legacy-07). A flat-zero {@link cortexAuthCheckerWouldDenyTotal}
|
|
63
|
+
* is ambiguous on its own: it can mean "no traffic would be denied" OR "the
|
|
64
|
+
* checker never ran" (no `@Authorized()` field was ever executed) — exactly the
|
|
65
|
+
* false-confidence pattern that preceded the beta-net-cap fail-open. Graduation
|
|
66
|
+
* rule: enforce only when this counter is > 0 and the would-deny series has
|
|
67
|
+
* been observed over a full trading week.
|
|
68
|
+
*/
|
|
69
|
+
export declare const cortexAuthCheckerEvaluationsTotal: Counter<string>;
|
|
60
70
|
/**
|
|
61
71
|
* Counts operations that resolver-level authorization WOULD deny. In shadow mode
|
|
62
72
|
* this is the primary signal proving whether flipping `CORTEX_AUTHCHECKER_ENFORCE`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cortex-auth-checker.d.ts","sourceRoot":"","sources":["../../../src/auth/cortex-auth-checker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,cAAc,CAAC;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAQzD;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,+BAA+B,CAAC;AAK3E;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,OAAO,CAGT;AAMD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,mBAK1C,CAAC;AAEH,uEAAuE;AACvE,UAAU,cAAc;IACtB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;CACnC;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,GAAG,IAAI,EAClC,aAAa,EAAE,SAAS,MAAM,EAAE,GAC/B,cAAc,CAyBhB;AAMD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACrC;AAcD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,iBAAiB,
|
|
1
|
+
{"version":3,"file":"cortex-auth-checker.d.ts","sourceRoot":"","sources":["../../../src/auth/cortex-auth-checker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,cAAc,CAAC;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAQzD;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,+BAA+B,CAAC;AAK3E;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,OAAO,CAGT;AAMD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;AAEtE;;;;;;;;GAQG;AACH,eAAO,MAAM,iCAAiC,iBAI5C,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,mBAK1C,CAAC;AAEH,uEAAuE;AACvE,UAAU,cAAc;IACtB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;CACnC;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,GAAG,IAAI,EAClC,aAAa,EAAE,SAAS,MAAM,EAAE,GAC/B,cAAc,CAyBhB;AAMD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACrC;AAcD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,iBAAiB,CAkC5D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cortex-auth-checker.js","sourceRoot":"","sources":["../../../src/auth/cortex-auth-checker.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cortex-auth-checker.js","sourceRoot":"","sources":["../../../src/auth/cortex-auth-checker.ts"],"names":[],"mappings":"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"}
|
|
@@ -60,6 +60,20 @@ export function isAuthCheckerEnforced(env = process.env) {
|
|
|
60
60
|
const raw = (env[CORTEX_AUTHCHECKER_ENFORCE_ENV] ?? '').trim().toLowerCase();
|
|
61
61
|
return raw === 'true' || raw === '1';
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Counts EVERY AuthChecker invocation, regardless of outcome (audit
|
|
65
|
+
* B01-backend-legacy-07). A flat-zero {@link cortexAuthCheckerWouldDenyTotal}
|
|
66
|
+
* is ambiguous on its own: it can mean "no traffic would be denied" OR "the
|
|
67
|
+
* checker never ran" (no `@Authorized()` field was ever executed) — exactly the
|
|
68
|
+
* false-confidence pattern that preceded the beta-net-cap fail-open. Graduation
|
|
69
|
+
* rule: enforce only when this counter is > 0 and the would-deny series has
|
|
70
|
+
* been observed over a full trading week.
|
|
71
|
+
*/
|
|
72
|
+
export const cortexAuthCheckerEvaluationsTotal = new Counter({
|
|
73
|
+
name: 'cortex_authchecker_evaluations_total',
|
|
74
|
+
help: 'Total AuthChecker invocations, any outcome (zero means no @Authorized field was executed — distinct from "no would-denies")',
|
|
75
|
+
registers: [metricsRegistry],
|
|
76
|
+
});
|
|
63
77
|
/**
|
|
64
78
|
* Counts operations that resolver-level authorization WOULD deny. In shadow mode
|
|
65
79
|
* this is the primary signal proving whether flipping `CORTEX_AUTHCHECKER_ENFORCE`
|
|
@@ -123,6 +137,9 @@ function describeOperation(resolverData) {
|
|
|
123
137
|
* metric or log emission.
|
|
124
138
|
*/
|
|
125
139
|
export const cortexAuthChecker = (resolverData, roles) => {
|
|
140
|
+
// Unconditional: proves the checker is actually reachable (see
|
|
141
|
+
// cortexAuthCheckerEvaluationsTotal TSDoc / audit B01-backend-legacy-07).
|
|
142
|
+
cortexAuthCheckerEvaluationsTotal.inc();
|
|
126
143
|
const principal = resolverData.context.principal ?? null;
|
|
127
144
|
const evaluation = evaluateWouldAllow(principal, roles);
|
|
128
145
|
if (evaluation.wouldAllow) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import type { Counter } from 'prom-client';
|
|
3
|
+
import type { Request, Response } from 'express';
|
|
4
|
+
import type { RateLimitConfig } from '../rate-limiter';
|
|
5
|
+
/**
|
|
6
|
+
* Shared fixtures for the rate-limiter test files (audit
|
|
7
|
+
* B01-backend-legacy-08). Pure helpers only — each test file remains
|
|
8
|
+
* responsible for setting `process.env.JWT_SECRET` via `vi.hoisted` BEFORE its
|
|
9
|
+
* first import pulls in `config/jwtConfig` (which reads the env at load time).
|
|
10
|
+
*/
|
|
11
|
+
/** Window used by every test config. */
|
|
12
|
+
export declare const WINDOW_MS = 60000;
|
|
13
|
+
/**
|
|
14
|
+
* Builds a small test limiter config.
|
|
15
|
+
*
|
|
16
|
+
* @param overrides - Field overrides applied over the defaults.
|
|
17
|
+
* @returns A complete {@link RateLimitConfig}.
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeConfig(overrides?: Partial<RateLimitConfig>): RateLimitConfig;
|
|
20
|
+
/** A mock Express response with observable spies. */
|
|
21
|
+
export interface MockResponse {
|
|
22
|
+
res: Response;
|
|
23
|
+
setHeader: ReturnType<typeof vi.fn>;
|
|
24
|
+
status: ReturnType<typeof vi.fn>;
|
|
25
|
+
json: ReturnType<typeof vi.fn>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates a mock response whose header/status/json calls are observable —
|
|
29
|
+
* the shadow contract is proven by these spies staying untouched.
|
|
30
|
+
*
|
|
31
|
+
* @returns The {@link MockResponse}.
|
|
32
|
+
*/
|
|
33
|
+
export declare function makeRes(): MockResponse;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a mock request. Defaults to an anonymous POST from a fixed IP.
|
|
36
|
+
*
|
|
37
|
+
* @param overrides - Request field overrides (headers, ip, method, …).
|
|
38
|
+
* @returns A Request usable by the limiter middleware.
|
|
39
|
+
*/
|
|
40
|
+
export declare function makeReq(overrides?: Record<string, unknown>): Request;
|
|
41
|
+
/**
|
|
42
|
+
* Reads the current value of a limiter counter for a label pair.
|
|
43
|
+
*
|
|
44
|
+
* @param counter - The prom-client counter to read.
|
|
45
|
+
* @param labels - The limiter/tier label pair identifying the series.
|
|
46
|
+
* @returns The current value (0 when the series does not exist yet).
|
|
47
|
+
*/
|
|
48
|
+
export declare function counterValue(counter: Counter<'limiter' | 'tier'>, labels: {
|
|
49
|
+
limiter: string;
|
|
50
|
+
tier: string;
|
|
51
|
+
}): Promise<number>;
|
|
52
|
+
/**
|
|
53
|
+
* Signs a real HS256 user JWT with the ambient `JWT_SECRET` (the same secret
|
|
54
|
+
* `config/jwtConfig` resolved at load), so the tier resolver verifies it.
|
|
55
|
+
*
|
|
56
|
+
* @param sub - Subject claim for the token.
|
|
57
|
+
* @returns A signed JWT string.
|
|
58
|
+
*/
|
|
59
|
+
export declare function signUserToken(sub: string): string;
|
|
60
|
+
//# sourceMappingURL=rate-limit-test-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-test-utils.d.ts","sourceRoot":"","sources":["../../../../src/middleware/__tests__/rate-limit-test-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE5B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;;;;GAKG;AAEH,wCAAwC;AACxC,eAAO,MAAM,SAAS,QAAS,CAAC;AAEhC;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,SAAS,GAAE,OAAO,CAAC,eAAe,CAAM,GACvC,eAAe,CAUjB;AAED,qDAAqD;AACrD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,QAAQ,CAAC;IACd,SAAS,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACpC,MAAM,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACjC,IAAI,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,IAAI,YAAY,CAMtC;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAQxE;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,EACpC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASjD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-test-utils.js","sourceRoot":"","sources":["../../../../src/middleware/__tests__/rate-limit-test-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC5B,OAAO,GAAG,MAAM,cAAc,CAAC;AAK/B;;;;;GAKG;AAEH,wCAAwC;AACxC,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,YAAsC,EAAE;IAExC,OAAO;QACL,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE,SAAS;QACnB,gBAAgB,EAAE,GAAG;QACrB,kBAAkB,EAAE,CAAC;QACrB,eAAe,EAAE,IAAI;QACrB,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC,EAAE;QACvD,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAUD;;;;;GAKG;AACH,MAAM,UAAU,OAAO;IACrB,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;IAC1B,MAAM,IAAI,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;IACrB,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvC,MAAM,GAAG,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAyB,CAAC;IAC/D,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CAAC,YAAqC,EAAE;IAC7D,OAAO;QACL,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE;QACX,EAAE,EAAE,cAAc;QAClB,MAAM,EAAE,EAAE,aAAa,EAAE,cAAc,EAAE;QACzC,GAAG,SAAS;KACS,CAAC;AAC1B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAoC,EACpC,MAAyC;IAEzC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC;IACnC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAC/B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAC5E,CAAC;IACF,OAAO,MAAM,EAAE,KAAK,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE;QAChD,SAAS,EAAE,OAAO;QAClB,SAAS,EAAE,IAAI;KAChB,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
/**
|
|
4
|
+
* Shared fixtures for the rate-limiter test files (audit
|
|
5
|
+
* B01-backend-legacy-08). Pure helpers only — each test file remains
|
|
6
|
+
* responsible for setting `process.env.JWT_SECRET` via `vi.hoisted` BEFORE its
|
|
7
|
+
* first import pulls in `config/jwtConfig` (which reads the env at load time).
|
|
8
|
+
*/
|
|
9
|
+
/** Window used by every test config. */
|
|
10
|
+
export const WINDOW_MS = 60_000;
|
|
11
|
+
/**
|
|
12
|
+
* Builds a small test limiter config.
|
|
13
|
+
*
|
|
14
|
+
* @param overrides - Field overrides applied over the defaults.
|
|
15
|
+
* @returns A complete {@link RateLimitConfig}.
|
|
16
|
+
*/
|
|
17
|
+
export function makeConfig(overrides = {}) {
|
|
18
|
+
return {
|
|
19
|
+
name: 'graphql',
|
|
20
|
+
windowMs: WINDOW_MS,
|
|
21
|
+
maxAuthenticated: 100,
|
|
22
|
+
maxUnauthenticated: 2,
|
|
23
|
+
standardHeaders: true,
|
|
24
|
+
message: { errors: [{ message: 'Too many requests' }] },
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Creates a mock response whose header/status/json calls are observable —
|
|
30
|
+
* the shadow contract is proven by these spies staying untouched.
|
|
31
|
+
*
|
|
32
|
+
* @returns The {@link MockResponse}.
|
|
33
|
+
*/
|
|
34
|
+
export function makeRes() {
|
|
35
|
+
const setHeader = vi.fn();
|
|
36
|
+
const json = vi.fn();
|
|
37
|
+
const status = vi.fn(() => ({ json }));
|
|
38
|
+
const res = { setHeader, status, json };
|
|
39
|
+
return { res, setHeader, status, json };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Creates a mock request. Defaults to an anonymous POST from a fixed IP.
|
|
43
|
+
*
|
|
44
|
+
* @param overrides - Request field overrides (headers, ip, method, …).
|
|
45
|
+
* @returns A Request usable by the limiter middleware.
|
|
46
|
+
*/
|
|
47
|
+
export function makeReq(overrides = {}) {
|
|
48
|
+
return {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {},
|
|
51
|
+
ip: '203.0.113.10',
|
|
52
|
+
socket: { remoteAddress: '203.0.113.10' },
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reads the current value of a limiter counter for a label pair.
|
|
58
|
+
*
|
|
59
|
+
* @param counter - The prom-client counter to read.
|
|
60
|
+
* @param labels - The limiter/tier label pair identifying the series.
|
|
61
|
+
* @returns The current value (0 when the series does not exist yet).
|
|
62
|
+
*/
|
|
63
|
+
export async function counterValue(counter, labels) {
|
|
64
|
+
const metric = await counter.get();
|
|
65
|
+
const sample = metric.values.find((v) => v.labels.limiter === labels.limiter && v.labels.tier === labels.tier);
|
|
66
|
+
return sample?.value ?? 0;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Signs a real HS256 user JWT with the ambient `JWT_SECRET` (the same secret
|
|
70
|
+
* `config/jwtConfig` resolved at load), so the tier resolver verifies it.
|
|
71
|
+
*
|
|
72
|
+
* @param sub - Subject claim for the token.
|
|
73
|
+
* @returns A signed JWT string.
|
|
74
|
+
*/
|
|
75
|
+
export function signUserToken(sub) {
|
|
76
|
+
const secret = process.env.JWT_SECRET;
|
|
77
|
+
if (!secret) {
|
|
78
|
+
throw new Error('JWT_SECRET must be set by the test file before signing');
|
|
79
|
+
}
|
|
80
|
+
return jwt.sign({ sub, roles: ['user'] }, secret, {
|
|
81
|
+
algorithm: 'HS256',
|
|
82
|
+
expiresIn: '1h',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=rate-limit-test-utils.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
-
import { Counter } from 'prom-client';
|
|
2
|
+
import { Counter, Gauge } from 'prom-client';
|
|
3
3
|
/**
|
|
4
4
|
* Environment variable gating 429 enforcement. When set to `true`/`1`, the
|
|
5
5
|
* limiters block over-limit requests with HTTP 429. Unset or any other value
|
|
@@ -16,24 +16,78 @@ export declare const CORTEX_RATE_LIMIT_ENFORCE_ENV = "CORTEX_RATE_LIMIT_ENFORCE"
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function isRateLimitEnforced(env?: Record<string, string | undefined>): boolean;
|
|
18
18
|
/**
|
|
19
|
-
* Counts requests that rate limiting WOULD block (HTTP 429)
|
|
20
|
-
* is the signal proving whether flipping
|
|
21
|
-
* would reject legitimate traffic.
|
|
22
|
-
* (`graphql`/`auth`) and the request `tier`
|
|
19
|
+
* Counts requests that rate limiting WOULD block (HTTP 429) while in SHADOW
|
|
20
|
+
* mode. This is the signal proving whether flipping
|
|
21
|
+
* `CORTEX_RATE_LIMIT_ENFORCE` to enforce would reject legitimate traffic.
|
|
22
|
+
* Labelled by the `limiter` name (`graphql`/`auth`) and the request `tier`
|
|
23
|
+
* (`auth`/`anon`). Real enforce-mode denials count on
|
|
24
|
+
* {@link cortexRateLimitBlockedTotal} instead, keeping the observed-only and
|
|
25
|
+
* actually-blocked series distinct (audit B01-backend-legacy-14).
|
|
23
26
|
*/
|
|
24
27
|
export declare const cortexRateLimitWouldBlockTotal: Counter<"limiter" | "tier">;
|
|
28
|
+
/**
|
|
29
|
+
* Counts requests actually blocked with HTTP 429 in ENFORCE mode. Kept as a
|
|
30
|
+
* separate series from {@link cortexRateLimitWouldBlockTotal} so shadow
|
|
31
|
+
* observations and real blocks never mix (audit B01-backend-legacy-14).
|
|
32
|
+
*/
|
|
33
|
+
export declare const cortexRateLimitBlockedTotal: Counter<"limiter" | "tier">;
|
|
34
|
+
/**
|
|
35
|
+
* Tracked bucket-key cardinality per limiter store. Bounds memory ahead of the
|
|
36
|
+
* trust-proxy fix, after which the identifier derives from X-Forwarded-For and
|
|
37
|
+
* an attacker rotating spoofed values could otherwise mint unbounded entries
|
|
38
|
+
* (audit B01-backend-legacy-12).
|
|
39
|
+
*/
|
|
40
|
+
export declare const cortexRateLimitStoreKeys: Gauge<"limiter">;
|
|
41
|
+
/** Configuration for one rate limiter instance. */
|
|
42
|
+
export interface RateLimitConfig {
|
|
43
|
+
/** Stable limiter identifier used as the `limiter` metric label. */
|
|
44
|
+
name: string;
|
|
45
|
+
windowMs: number;
|
|
46
|
+
maxAuthenticated: number;
|
|
47
|
+
maxUnauthenticated: number;
|
|
48
|
+
message: {
|
|
49
|
+
errors: Array<{
|
|
50
|
+
message: string;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
53
|
+
standardHeaders?: boolean;
|
|
54
|
+
legacyHeaders?: boolean;
|
|
55
|
+
/** Cap on distinct bucket keys (defaults to {@link DEFAULT_MAX_TRACKED_KEYS}). */
|
|
56
|
+
maxTrackedKeys?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Creates an in-memory rate limiter middleware with separate limits for
|
|
60
|
+
* verified (auth) and anonymous requests.
|
|
61
|
+
*
|
|
62
|
+
* Bucketing: verified callers are keyed per principal (`sub:<sub>`); anonymous
|
|
63
|
+
* callers are keyed by client IP, which resolves correctly behind the LB only
|
|
64
|
+
* with `trust proxy` configured in server.ts (audit B01-backend-legacy-02).
|
|
65
|
+
* The `auth` tier requires actual offline verification — a fabricated
|
|
66
|
+
* JWT-shaped header no longer earns the higher budget (B01-backend-legacy-09).
|
|
67
|
+
* CORS preflights (`OPTIONS`) are never counted (B01-backend-legacy-11).
|
|
68
|
+
*
|
|
69
|
+
* Response headers (enforce mode only, when standardHeaders is enabled):
|
|
70
|
+
* X-RateLimit-Limit - maximum requests allowed in the current window
|
|
71
|
+
* X-RateLimit-Remaining - requests remaining in the current window
|
|
72
|
+
* X-RateLimit-Reset - seconds until the current window resets
|
|
73
|
+
* Retry-After - seconds to wait before retrying (only on 429)
|
|
74
|
+
*
|
|
75
|
+
* @param config - Rate limit configuration.
|
|
76
|
+
* @returns Express middleware function.
|
|
77
|
+
*/
|
|
78
|
+
export declare function createRateLimiter(config: RateLimitConfig): (req: Request, res: Response, next: NextFunction) => void;
|
|
25
79
|
/**
|
|
26
80
|
* Rate limiter for GraphQL endpoint.
|
|
27
81
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
82
|
+
* Verified requests: 1000 requests per 15 minutes (configurable via RATE_LIMIT_MAX)
|
|
83
|
+
* Anonymous requests: 200 requests per 15 minutes (configurable via RATE_LIMIT_MAX_UNAUTH)
|
|
30
84
|
*/
|
|
31
85
|
export declare const graphqlRateLimiter: (req: Request, res: Response, next: NextFunction) => void;
|
|
32
86
|
/**
|
|
33
87
|
* Rate limiter for authentication endpoints.
|
|
34
88
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
89
|
+
* Verified requests: 50 requests per 15 minutes
|
|
90
|
+
* Anonymous requests: 20 requests per 15 minutes
|
|
37
91
|
*/
|
|
38
92
|
export declare const authRateLimiter: (req: Request, res: Response, next: NextFunction) => void;
|
|
39
93
|
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../../src/middleware/rate-limiter.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../../src/middleware/rate-limiter.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAK7C;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,8BAA8B,CAAC;AAEzE;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,OAAO,CAGT;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,6BAKzC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,6BAKtC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,kBAKnC,CAAC;AAeH,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE;QAAE,MAAM,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;IAChD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAOD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,eAAe,GACtB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAgG3D;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAxGtB,OAAO,OAAO,QAAQ,QAAQ,YAAY,KAAK,IAkHtD,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QA1HnB,OAAO,OAAO,QAAQ,QAAQ,YAAY,KAAK,IAkItD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../../src/middleware/rate-limiter.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../../src/middleware/rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// CORTEX-P0-001: mounted in server.ts as
|
|
2
|
-
// app.use('/graphql', graphqlRateLimiter)
|
|
3
|
-
// app.use('/api', authRateLimiter)
|
|
2
|
+
// app.use('/graphql', cors, graphqlRateLimiter, …) // GraphQL surface (after CORS)
|
|
3
|
+
// app.use('/api', authRateLimiter) // authenticated Express surface
|
|
4
4
|
// SHADOW-FIRST: 429 enforcement is gated behind CORTEX_RATE_LIMIT_ENFORCE.
|
|
5
5
|
// While OFF (default), the limiters observe + count requests that WOULD be
|
|
6
6
|
// blocked but never touch the response — live behaviour is byte-identical.
|
|
7
|
-
import { Counter } from 'prom-client';
|
|
7
|
+
import { Counter, Gauge } from 'prom-client';
|
|
8
8
|
import { metricsRegistry } from '../config/metrics.mjs';
|
|
9
9
|
import { logger } from '../utils/logger.mjs';
|
|
10
|
+
import { resolveRateTier } from './rate-tier.mjs';
|
|
10
11
|
/**
|
|
11
12
|
* Environment variable gating 429 enforcement. When set to `true`/`1`, the
|
|
12
13
|
* limiters block over-limit requests with HTTP 429. Unset or any other value
|
|
@@ -26,78 +27,116 @@ export function isRateLimitEnforced(env = process.env) {
|
|
|
26
27
|
return raw === 'true' || raw === '1';
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
|
-
* Counts requests that rate limiting WOULD block (HTTP 429)
|
|
30
|
-
* is the signal proving whether flipping
|
|
31
|
-
* would reject legitimate traffic.
|
|
32
|
-
* (`graphql`/`auth`) and the request `tier`
|
|
30
|
+
* Counts requests that rate limiting WOULD block (HTTP 429) while in SHADOW
|
|
31
|
+
* mode. This is the signal proving whether flipping
|
|
32
|
+
* `CORTEX_RATE_LIMIT_ENFORCE` to enforce would reject legitimate traffic.
|
|
33
|
+
* Labelled by the `limiter` name (`graphql`/`auth`) and the request `tier`
|
|
34
|
+
* (`auth`/`anon`). Real enforce-mode denials count on
|
|
35
|
+
* {@link cortexRateLimitBlockedTotal} instead, keeping the observed-only and
|
|
36
|
+
* actually-blocked series distinct (audit B01-backend-legacy-14).
|
|
33
37
|
*/
|
|
34
38
|
export const cortexRateLimitWouldBlockTotal = new Counter({
|
|
35
39
|
name: 'cortex_rate_limit_would_block_total',
|
|
36
|
-
help: 'Requests rate limiting would block (429), by limiter and tier (
|
|
40
|
+
help: 'Requests rate limiting would block (429), by limiter and tier (shadow mode only; real denials count on cortex_rate_limit_blocked_total)',
|
|
37
41
|
labelNames: ['limiter', 'tier'],
|
|
38
42
|
registers: [metricsRegistry],
|
|
39
43
|
});
|
|
40
44
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* (standard JWT structure).
|
|
45
|
+
* Counts requests actually blocked with HTTP 429 in ENFORCE mode. Kept as a
|
|
46
|
+
* separate series from {@link cortexRateLimitWouldBlockTotal} so shadow
|
|
47
|
+
* observations and real blocks never mix (audit B01-backend-legacy-14).
|
|
45
48
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
export const cortexRateLimitBlockedTotal = new Counter({
|
|
50
|
+
name: 'cortex_rate_limit_blocked_total',
|
|
51
|
+
help: 'Requests actually blocked with HTTP 429, by limiter and tier (enforce mode only)',
|
|
52
|
+
labelNames: ['limiter', 'tier'],
|
|
53
|
+
registers: [metricsRegistry],
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* Tracked bucket-key cardinality per limiter store. Bounds memory ahead of the
|
|
57
|
+
* trust-proxy fix, after which the identifier derives from X-Forwarded-For and
|
|
58
|
+
* an attacker rotating spoofed values could otherwise mint unbounded entries
|
|
59
|
+
* (audit B01-backend-legacy-12).
|
|
60
|
+
*/
|
|
61
|
+
export const cortexRateLimitStoreKeys = new Gauge({
|
|
62
|
+
name: 'cortex_rate_limit_store_keys',
|
|
63
|
+
help: 'Current tracked bucket-key cardinality per limiter store',
|
|
64
|
+
labelNames: ['limiter'],
|
|
65
|
+
registers: [metricsRegistry],
|
|
66
|
+
});
|
|
67
|
+
/** Sweep cadence for expired bucket entries. */
|
|
68
|
+
const STORE_SWEEP_INTERVAL_MS = 60_000;
|
|
58
69
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
70
|
+
* Default cap on distinct bucket keys per limiter store. Past the cap, new
|
|
71
|
+
* identifiers aggregate into a shared per-tier overflow bucket instead of
|
|
72
|
+
* allocating — bounding memory under identifier-rotation abuse.
|
|
73
|
+
*/
|
|
74
|
+
const DEFAULT_MAX_TRACKED_KEYS = 100_000;
|
|
75
|
+
/** Bucket-key prefix used once the store cardinality cap is reached. */
|
|
76
|
+
const OVERFLOW_KEY_PREFIX = 'overflow';
|
|
77
|
+
/**
|
|
78
|
+
* Creates an in-memory rate limiter middleware with separate limits for
|
|
79
|
+
* verified (auth) and anonymous requests.
|
|
61
80
|
*
|
|
62
|
-
*
|
|
81
|
+
* Bucketing: verified callers are keyed per principal (`sub:<sub>`); anonymous
|
|
82
|
+
* callers are keyed by client IP, which resolves correctly behind the LB only
|
|
83
|
+
* with `trust proxy` configured in server.ts (audit B01-backend-legacy-02).
|
|
84
|
+
* The `auth` tier requires actual offline verification — a fabricated
|
|
85
|
+
* JWT-shaped header no longer earns the higher budget (B01-backend-legacy-09).
|
|
86
|
+
* CORS preflights (`OPTIONS`) are never counted (B01-backend-legacy-11).
|
|
87
|
+
*
|
|
88
|
+
* Response headers (enforce mode only, when standardHeaders is enabled):
|
|
63
89
|
* X-RateLimit-Limit - maximum requests allowed in the current window
|
|
64
90
|
* X-RateLimit-Remaining - requests remaining in the current window
|
|
65
91
|
* X-RateLimit-Reset - seconds until the current window resets
|
|
66
92
|
* Retry-After - seconds to wait before retrying (only on 429)
|
|
67
93
|
*
|
|
68
|
-
* @param config - Rate limit configuration
|
|
69
|
-
* @returns Express middleware function
|
|
94
|
+
* @param config - Rate limit configuration.
|
|
95
|
+
* @returns Express middleware function.
|
|
70
96
|
*/
|
|
71
|
-
function createRateLimiter(config) {
|
|
72
|
-
const store =
|
|
73
|
-
|
|
97
|
+
export function createRateLimiter(config) {
|
|
98
|
+
const store = new Map();
|
|
99
|
+
const maxTrackedKeys = config.maxTrackedKeys ?? DEFAULT_MAX_TRACKED_KEYS;
|
|
100
|
+
// Sweep expired entries every minute. `unref()` so an importing process
|
|
101
|
+
// (tests, one-off scripts) is never kept alive by the sweeper
|
|
102
|
+
// (audit B01-backend-legacy-12).
|
|
74
103
|
setInterval(() => {
|
|
75
104
|
const now = Date.now();
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
delete
|
|
105
|
+
for (const [key, entry] of store) {
|
|
106
|
+
if (entry.resetTime < now) {
|
|
107
|
+
store.delete(key);
|
|
79
108
|
}
|
|
80
|
-
}
|
|
81
|
-
|
|
109
|
+
}
|
|
110
|
+
cortexRateLimitStoreKeys.set({ limiter: config.name }, store.size);
|
|
111
|
+
}, STORE_SWEEP_INTERVAL_MS).unref();
|
|
82
112
|
return (req, res, next) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
113
|
+
// CORS preflights carry no credentials and must not consume rate budget
|
|
114
|
+
// (audit B01-backend-legacy-11).
|
|
115
|
+
if (req.method === 'OPTIONS') {
|
|
116
|
+
next();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const { tier, principalKey } = resolveRateTier(req);
|
|
120
|
+
const identifier = principalKey ?? req.ip ?? req.socket.remoteAddress ?? 'unknown';
|
|
121
|
+
const effectiveMax = tier === 'auth' ? config.maxAuthenticated : config.maxUnauthenticated;
|
|
122
|
+
let storeKey = `${identifier}:${tier}`;
|
|
90
123
|
const now = Date.now();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
124
|
+
// Cardinality cap: aggregate novel identifiers into a shared per-tier
|
|
125
|
+
// overflow bucket rather than growing without bound
|
|
126
|
+
// (audit B01-backend-legacy-12).
|
|
127
|
+
if (!store.has(storeKey) && store.size >= maxTrackedKeys) {
|
|
128
|
+
storeKey = `${OVERFLOW_KEY_PREFIX}:${tier}`;
|
|
129
|
+
}
|
|
130
|
+
const existing = store.get(storeKey);
|
|
131
|
+
let current;
|
|
132
|
+
if (!existing || existing.resetTime < now) {
|
|
133
|
+
current = { count: 1, resetTime: now + config.windowMs };
|
|
134
|
+
store.set(storeKey, current);
|
|
96
135
|
}
|
|
97
136
|
else {
|
|
98
|
-
|
|
137
|
+
existing.count += 1;
|
|
138
|
+
current = existing;
|
|
99
139
|
}
|
|
100
|
-
const current = store[storeKey];
|
|
101
140
|
const remaining = Math.max(0, effectiveMax - current.count);
|
|
102
141
|
const resetSeconds = Math.ceil((current.resetTime - now) / 1000);
|
|
103
142
|
const overLimit = current.count > effectiveMax;
|
|
@@ -107,14 +146,19 @@ function createRateLimiter(config) {
|
|
|
107
146
|
if (!isRateLimitEnforced()) {
|
|
108
147
|
if (overLimit) {
|
|
109
148
|
cortexRateLimitWouldBlockTotal.inc({ limiter: config.name, tier });
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
149
|
+
// Log only the FIRST over-limit request per (bucket, window) — the
|
|
150
|
+
// counter carries per-request cardinality; unthrottled logging floods
|
|
151
|
+
// Cloud Logging and buries real warnings (audit B01-backend-legacy-10).
|
|
152
|
+
if (current.count === effectiveMax + 1) {
|
|
153
|
+
logger.warn('[cortex-rate-limit] shadow would-block (allowing)', {
|
|
154
|
+
limiter: config.name,
|
|
155
|
+
identifier,
|
|
156
|
+
tier,
|
|
157
|
+
count: current.count,
|
|
158
|
+
limit: effectiveMax,
|
|
159
|
+
resetSeconds,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
118
162
|
}
|
|
119
163
|
next();
|
|
120
164
|
return;
|
|
@@ -129,7 +173,7 @@ function createRateLimiter(config) {
|
|
|
129
173
|
if (overLimit) {
|
|
130
174
|
// Include Retry-After header on 429 responses (RFC 6585 / RFC 7231)
|
|
131
175
|
res.setHeader('Retry-After', resetSeconds.toString());
|
|
132
|
-
|
|
176
|
+
cortexRateLimitBlockedTotal.inc({ limiter: config.name, tier });
|
|
133
177
|
res.status(429).json(config.message);
|
|
134
178
|
return;
|
|
135
179
|
}
|
|
@@ -139,8 +183,8 @@ function createRateLimiter(config) {
|
|
|
139
183
|
/**
|
|
140
184
|
* Rate limiter for GraphQL endpoint.
|
|
141
185
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
186
|
+
* Verified requests: 1000 requests per 15 minutes (configurable via RATE_LIMIT_MAX)
|
|
187
|
+
* Anonymous requests: 200 requests per 15 minutes (configurable via RATE_LIMIT_MAX_UNAUTH)
|
|
144
188
|
*/
|
|
145
189
|
export const graphqlRateLimiter = createRateLimiter({
|
|
146
190
|
name: 'graphql',
|
|
@@ -156,8 +200,8 @@ export const graphqlRateLimiter = createRateLimiter({
|
|
|
156
200
|
/**
|
|
157
201
|
* Rate limiter for authentication endpoints.
|
|
158
202
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
203
|
+
* Verified requests: 50 requests per 15 minutes
|
|
204
|
+
* Anonymous requests: 20 requests per 15 minutes
|
|
161
205
|
*/
|
|
162
206
|
export const authRateLimiter = createRateLimiter({
|
|
163
207
|
name: 'auth',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limit tier resolution (audit B01-backend-legacy-09).
|
|
3
|
+
*
|
|
4
|
+
* The rate limiter previously granted the 5x-higher authenticated tier to any
|
|
5
|
+
* request carrying a 3-dot-segment `Bearer` header — a trivially fabricated
|
|
6
|
+
* shape (`Bearer a.b.c`). This module keys the tier off ACTUAL verification
|
|
7
|
+
* instead, using the two offline-verifiable credential classes:
|
|
8
|
+
*
|
|
9
|
+
* 1. `SERVER_AUTH_TOKEN` exact match (the engine / server-to-server path).
|
|
10
|
+
* 2. App-issued HS256 JWTs verified against the shared `jwtSecret`.
|
|
11
|
+
*
|
|
12
|
+
* Google ID tokens (RS256, JWKS-verified) cannot be verified synchronously
|
|
13
|
+
* without a network dependency in the request hot path; they are deliberately
|
|
14
|
+
* tiered as `anon`. This is a metrics-only inaccuracy while the limiter is in
|
|
15
|
+
* shadow mode — before any enforce graduation, either extend this resolver
|
|
16
|
+
* with an async JWKS-backed path or raise the anon ceiling to cover browser
|
|
17
|
+
* traffic (tracked in the B01-02 graduation checklist).
|
|
18
|
+
*
|
|
19
|
+
* Verified results are memoised in a bounded, TTL'd cache keyed by the token's
|
|
20
|
+
* SHA-256 (raw tokens are never retained) so the per-request cost is one hash.
|
|
21
|
+
*/
|
|
22
|
+
import type { Request } from 'express';
|
|
23
|
+
/** Rate-limit tier: `auth` = verified caller budget, `anon` = default budget. */
|
|
24
|
+
export type RateTier = 'auth' | 'anon';
|
|
25
|
+
/**
|
|
26
|
+
* Outcome of tier resolution for one request.
|
|
27
|
+
*/
|
|
28
|
+
export interface RateTierResolution {
|
|
29
|
+
/** The tier the request's rate budget is drawn from. */
|
|
30
|
+
readonly tier: RateTier;
|
|
31
|
+
/**
|
|
32
|
+
* Stable per-principal bucket key (`sub:<sub>`) for verified callers, so
|
|
33
|
+
* authenticated traffic is bucketed per principal rather than per IP
|
|
34
|
+
* (audit B01-backend-legacy-02). `null` for anonymous callers — the limiter
|
|
35
|
+
* falls back to the client IP.
|
|
36
|
+
*/
|
|
37
|
+
readonly principalKey: string | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Clears the memoised token-verdict cache. Test-only.
|
|
41
|
+
*
|
|
42
|
+
* @internal exported for testing
|
|
43
|
+
*/
|
|
44
|
+
export declare function _resetRateTierCacheForTests(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the rate-limit tier and principal bucket key for a request.
|
|
47
|
+
*
|
|
48
|
+
* Cheap on the hot path: one SHA-256 over the token plus a bounded map lookup;
|
|
49
|
+
* the HMAC verification runs at most once per token per TTL window.
|
|
50
|
+
*
|
|
51
|
+
* @param req - Incoming Express request.
|
|
52
|
+
* @returns The request's {@link RateTierResolution}; `anon` when no `Bearer`
|
|
53
|
+
* token is present or the token cannot be verified offline.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveRateTier(req: Request): RateTierResolution;
|
|
56
|
+
//# sourceMappingURL=rate-tier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-tier.d.ts","sourceRoot":"","sources":["../../../src/middleware/rate-tier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC,iFAAiF;AACjF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAiBD;;;;GAIG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAiCD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CA6BhE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-tier.js","sourceRoot":"","sources":["../../../src/middleware/rate-tier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limit tier resolution (audit B01-backend-legacy-09).
|
|
3
|
+
*
|
|
4
|
+
* The rate limiter previously granted the 5x-higher authenticated tier to any
|
|
5
|
+
* request carrying a 3-dot-segment `Bearer` header — a trivially fabricated
|
|
6
|
+
* shape (`Bearer a.b.c`). This module keys the tier off ACTUAL verification
|
|
7
|
+
* instead, using the two offline-verifiable credential classes:
|
|
8
|
+
*
|
|
9
|
+
* 1. `SERVER_AUTH_TOKEN` exact match (the engine / server-to-server path).
|
|
10
|
+
* 2. App-issued HS256 JWTs verified against the shared `jwtSecret`.
|
|
11
|
+
*
|
|
12
|
+
* Google ID tokens (RS256, JWKS-verified) cannot be verified synchronously
|
|
13
|
+
* without a network dependency in the request hot path; they are deliberately
|
|
14
|
+
* tiered as `anon`. This is a metrics-only inaccuracy while the limiter is in
|
|
15
|
+
* shadow mode — before any enforce graduation, either extend this resolver
|
|
16
|
+
* with an async JWKS-backed path or raise the anon ceiling to cover browser
|
|
17
|
+
* traffic (tracked in the B01-02 graduation checklist).
|
|
18
|
+
*
|
|
19
|
+
* Verified results are memoised in a bounded, TTL'd cache keyed by the token's
|
|
20
|
+
* SHA-256 (raw tokens are never retained) so the per-request cost is one hash.
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from 'crypto';
|
|
23
|
+
import jwt from 'jsonwebtoken';
|
|
24
|
+
import { jwtSecret } from '../config/jwtConfig.mjs';
|
|
25
|
+
/** Bound on memoised token verdicts; oldest entries evicted first. */
|
|
26
|
+
const TIER_CACHE_MAX_ENTRIES = 2048;
|
|
27
|
+
/** Memoised verdicts expire after one rate-limit window (15 minutes). */
|
|
28
|
+
const TIER_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
29
|
+
const ANON_RESOLUTION = { tier: 'anon', principalKey: null };
|
|
30
|
+
const tierCache = new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Clears the memoised token-verdict cache. Test-only.
|
|
33
|
+
*
|
|
34
|
+
* @internal exported for testing
|
|
35
|
+
*/
|
|
36
|
+
export function _resetRateTierCacheForTests() {
|
|
37
|
+
tierCache.clear();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Verifies a bearer token offline and classifies its rate tier.
|
|
41
|
+
*
|
|
42
|
+
* @param token - Raw bearer token value (after the `Bearer ` prefix).
|
|
43
|
+
* @returns The resolved tier; `anon` for anything that fails verification.
|
|
44
|
+
*/
|
|
45
|
+
function verifyTier(token) {
|
|
46
|
+
const serverAuthToken = process.env.SERVER_AUTH_TOKEN;
|
|
47
|
+
if (typeof serverAuthToken === 'string' &&
|
|
48
|
+
serverAuthToken.length > 0 &&
|
|
49
|
+
token === serverAuthToken) {
|
|
50
|
+
return { tier: 'auth', principalKey: 'sub:server' };
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
// HS256 pinned for the same alg-confusion reasons as the token-verifier.
|
|
54
|
+
const payload = jwt.verify(token, jwtSecret, { algorithms: ['HS256'] });
|
|
55
|
+
if (typeof payload !== 'string' && typeof payload.sub === 'string') {
|
|
56
|
+
return { tier: 'auth', principalKey: `sub:${payload.sub}` };
|
|
57
|
+
}
|
|
58
|
+
return ANON_RESOLUTION;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Unverifiable (bad signature, expired, RS256 Google ID token, garbage
|
|
62
|
+
// shape) — treated as anonymous for tier purposes. Never throws upward:
|
|
63
|
+
// the limiter must not be able to fail a request.
|
|
64
|
+
return ANON_RESOLUTION;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolves the rate-limit tier and principal bucket key for a request.
|
|
69
|
+
*
|
|
70
|
+
* Cheap on the hot path: one SHA-256 over the token plus a bounded map lookup;
|
|
71
|
+
* the HMAC verification runs at most once per token per TTL window.
|
|
72
|
+
*
|
|
73
|
+
* @param req - Incoming Express request.
|
|
74
|
+
* @returns The request's {@link RateTierResolution}; `anon` when no `Bearer`
|
|
75
|
+
* token is present or the token cannot be verified offline.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveRateTier(req) {
|
|
78
|
+
const authHeader = req.headers.authorization ?? '';
|
|
79
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
80
|
+
return ANON_RESOLUTION;
|
|
81
|
+
}
|
|
82
|
+
const token = authHeader.slice('Bearer '.length).trim();
|
|
83
|
+
if (token.length === 0) {
|
|
84
|
+
return ANON_RESOLUTION;
|
|
85
|
+
}
|
|
86
|
+
const cacheKey = createHash('sha256').update(token).digest('hex');
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const cached = tierCache.get(cacheKey);
|
|
89
|
+
if (cached && cached.expiresAt > now) {
|
|
90
|
+
return cached.resolution;
|
|
91
|
+
}
|
|
92
|
+
const resolution = verifyTier(token);
|
|
93
|
+
if (tierCache.size >= TIER_CACHE_MAX_ENTRIES) {
|
|
94
|
+
// Map preserves insertion order — evict the oldest entry.
|
|
95
|
+
const oldestKey = tierCache.keys().next().value;
|
|
96
|
+
if (oldestKey !== undefined) {
|
|
97
|
+
tierCache.delete(oldestKey);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
tierCache.set(cacheKey, { resolution, expiresAt: now + TIER_CACHE_TTL_MS });
|
|
101
|
+
return resolution;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=rate-tier.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/utils/logger.ts"],"names":[],"mappings":"AAUA,UAAU,MAAM;IACd,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAChE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACjE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAClE;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/utils/logger.ts"],"names":[],"mappings":"AAUA,UAAU,MAAM;IACd,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAChE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACjE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAClE;AAmED,eAAO,MAAM,MAAM,QAAiC,CAAC"}
|
package/esm/utils/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../../src/utils/logger.ts"],"names":[],"mappings":"AAiBA,SAAS,cAAc,CACrB,KAAe,EACf,OAAe,EACf,OAAe,EACf,IAA8B;IAE9B,MAAM,KAAK,GAAa;QACtB,KAAK;QACL,OAAO;QACP,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,OAAO;QACP,GAAG,IAAI;KACR,CAAC;IACF,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,YAAY,CAAC,OAAe;IACnC,
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../../src/utils/logger.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,GAA6B;IAC3C,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,SAAS,eAAe;IACtB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC;IACjD,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;QAC3E,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,cAAc,GAAG,UAAU,CAAC,eAAe,EAAE,CAAC,CAAC;AAErD,SAAS,cAAc,CACrB,KAAe,EACf,OAAe,EACf,OAAe,EACf,IAA8B;IAE9B,MAAM,KAAK,GAAa;QACtB,KAAK;QACL,OAAO;QACP,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,OAAO;QACP,GAAG,IAAI;KACR,CAAC;IACF,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,KAAK,GAAG,CACZ,KAAe,EACf,MAA0B,EAC1B,OAAe,EACf,IAA8B,EACxB,EAAE;QACR,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,cAAc;YAAE,OAAO;QAC/C,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;IACrE,CAAC,CAAC;IACF,OAAO;QACL,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,EAAQ,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;QAC3E,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,EAAQ,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;QAC3E,KAAK,EAAE,CAAC,OAAO,EAAE,IAAI,EAAQ,EAAE,CAC7B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;QAC/C,KAAK,EAAE,CAAC,OAAO,EAAE,IAAI,EAAQ,EAAE,CAC7B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;KAChD,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,YAAY,CAAC,gBAAgB,CAAC,CAAC"}
|
package/esm/utils/logger.mjs
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimum-level filter, resolved once at module load from `LOG_LEVEL`.
|
|
3
|
+
*
|
|
4
|
+
* Default is `debug` — every call writes, byte-identical to the historical
|
|
5
|
+
* unconditional behaviour, so production output is unchanged unless an
|
|
6
|
+
* operator sets the variable. The test suite sets `LOG_LEVEL=error`
|
|
7
|
+
* (vitest.config.ts): the codegen pipeline emits tens of thousands of
|
|
8
|
+
* warn-level lines in-process, and under vitest that volume saturates the
|
|
9
|
+
* worker IPC channel until the run fails with an unhandled
|
|
10
|
+
* `[vitest-worker]: Timeout calling "onTaskUpdate"` even when every test
|
|
11
|
+
* passes (observed on publish-gate runs 31244014211 / 31249917514).
|
|
12
|
+
*/
|
|
13
|
+
const LEVEL_RANK = {
|
|
14
|
+
debug: 0,
|
|
15
|
+
info: 1,
|
|
16
|
+
warn: 2,
|
|
17
|
+
error: 3,
|
|
18
|
+
};
|
|
19
|
+
function resolveMinLevel() {
|
|
20
|
+
const raw = process.env.LOG_LEVEL?.toLowerCase();
|
|
21
|
+
if (raw === 'debug' || raw === 'info' || raw === 'warn' || raw === 'error') {
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
return 'debug';
|
|
25
|
+
}
|
|
26
|
+
const MIN_LEVEL_RANK = LEVEL_RANK[resolveMinLevel()];
|
|
1
27
|
function formatLogEntry(level, service, message, meta) {
|
|
2
28
|
const entry = {
|
|
3
29
|
level,
|
|
@@ -9,19 +35,16 @@ function formatLogEntry(level, service, message, meta) {
|
|
|
9
35
|
return JSON.stringify(entry);
|
|
10
36
|
}
|
|
11
37
|
function createLogger(service) {
|
|
38
|
+
const write = (level, stream, message, meta) => {
|
|
39
|
+
if (LEVEL_RANK[level] < MIN_LEVEL_RANK)
|
|
40
|
+
return;
|
|
41
|
+
stream.write(formatLogEntry(level, service, message, meta) + '\n');
|
|
42
|
+
};
|
|
12
43
|
return {
|
|
13
|
-
info: (message, meta) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
process.stdout.write(formatLogEntry('warn', service, message, meta) + '\n');
|
|
18
|
-
},
|
|
19
|
-
error: (message, meta) => {
|
|
20
|
-
process.stderr.write(formatLogEntry('error', service, message, meta) + '\n');
|
|
21
|
-
},
|
|
22
|
-
debug: (message, meta) => {
|
|
23
|
-
process.stdout.write(formatLogEntry('debug', service, message, meta) + '\n');
|
|
24
|
-
},
|
|
44
|
+
info: (message, meta) => write('info', process.stdout, message, meta),
|
|
45
|
+
warn: (message, meta) => write('warn', process.stdout, message, meta),
|
|
46
|
+
error: (message, meta) => write('error', process.stderr, message, meta),
|
|
47
|
+
debug: (message, meta) => write('debug', process.stdout, message, meta),
|
|
25
48
|
};
|
|
26
49
|
}
|
|
27
50
|
export const logger = createLogger('backend-legacy');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adaptic/backend-legacy",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.999",
|
|
4
4
|
"description": "Backend executable CRUD functions with dynamic variables construction, and type definitions for the Adaptic AI platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "index.d.ts",
|
package/server.cjs
CHANGED
|
@@ -63,6 +63,7 @@ const rate_limiter_1 = require("./middleware/rate-limiter.cjs");
|
|
|
63
63
|
const audit_logger_1 = require("./middleware/audit-logger.cjs");
|
|
64
64
|
const tenancy_scoping_1 = require("./middleware/tenancy-scoping.cjs");
|
|
65
65
|
const cortex_auth_checker_1 = require("./auth/cortex-auth-checker.cjs");
|
|
66
|
+
const authorization_map_1 = require("./auth/authorization-map.cjs");
|
|
66
67
|
const http_status_mapper_1 = require("./plugins/http-status-mapper.cjs");
|
|
67
68
|
const graphql_validation_plugin_1 = require("./middleware/graphql-validation-plugin.cjs");
|
|
68
69
|
const query_complexity_1 = require("./middleware/query-complexity.cjs");
|
|
@@ -102,7 +103,15 @@ function principalToUser(principal) {
|
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Default number of proxy hops trusted for X-Forwarded-For resolution when
|
|
108
|
+
* `TRUST_PROXY` is unset. One hop matches a single fronting LB/ingress proxy;
|
|
109
|
+
* operators must set `TRUST_PROXY` to the exact hop count of their deployment
|
|
110
|
+
* (audit B01-backend-legacy-02 / -12).
|
|
111
|
+
*/
|
|
112
|
+
const DEFAULT_TRUST_PROXY_HOPS = 1;
|
|
105
113
|
const startServer = async () => {
|
|
114
|
+
var _a;
|
|
106
115
|
// Boot-time invariant: in production, `GOOGLE_OAUTH_CLIENT_IDS` must be set.
|
|
107
116
|
// Without it, no Google ID token can be safely verified — and the verifier
|
|
108
117
|
// would surface a per-request `misconfigured` error indefinitely. Refuse to
|
|
@@ -112,6 +121,21 @@ const startServer = async () => {
|
|
|
112
121
|
// is on (defaults on in production/staging). Registers default Node.js metrics
|
|
113
122
|
// and starts the uptime gauge ticker. See: src/config/metrics.ts.
|
|
114
123
|
(0, metrics_1.initMetrics)();
|
|
124
|
+
// CORTEX-P0-001 phase 2 (audit B01-backend-legacy-03): decorate generated
|
|
125
|
+
// CRUD resolvers with @Authorized() — full coverage on the 5 investor-relations
|
|
126
|
+
// models, delete mutations elsewhere — so the authChecker below actually
|
|
127
|
+
// executes. Must run BEFORE buildSchema (decorators land in TypeGraphQL's
|
|
128
|
+
// metadata storage). The checker stays SHADOW-FIRST: would-denies are logged
|
|
129
|
+
// + counted but ALLOWED until CORTEX_AUTHCHECKER_ENFORCE is flipped on.
|
|
130
|
+
// The boot log satisfies audit B01-backend-legacy-07: a zero decorated-action
|
|
131
|
+
// count means the checker is unreachable and its metrics are meaningless.
|
|
132
|
+
const authzSummary = (0, authorization_map_1.applyCortexAuthorizationMap)();
|
|
133
|
+
logger_1.logger.info('[cortex-authz] applied @Authorized coverage (authChecker in shadow unless CORTEX_AUTHCHECKER_ENFORCE)', {
|
|
134
|
+
fullCoverageModels: authzSummary.fullCoverageModels,
|
|
135
|
+
deleteCoverageModels: authzSummary.deleteCoverageModels,
|
|
136
|
+
decoratedActions: authzSummary.decoratedActions,
|
|
137
|
+
skippedActions: authzSummary.skippedActions.length,
|
|
138
|
+
});
|
|
115
139
|
const schema = await (0, type_graphql_1.buildSchema)({
|
|
116
140
|
resolvers: [...typegraphql_prisma_1.resolvers, custom_1.OptionsGreeksHistoryCustomResolver],
|
|
117
141
|
validate: false,
|
|
@@ -120,23 +144,41 @@ const startServer = async () => {
|
|
|
120
144
|
// and unauthenticated callers are bypassed in every mode. Gated by
|
|
121
145
|
// `TENANCY_SCOPING_MODE` (default `shadow`).
|
|
122
146
|
globalMiddlewares: [(0, tenancy_scoping_1.createTenancyScopingMiddleware)()],
|
|
123
|
-
// Resolver-level authorization (CORTEX-P0-001).
|
|
124
|
-
//
|
|
125
|
-
// is OFF (default),
|
|
126
|
-
//
|
|
147
|
+
// Resolver-level authorization (CORTEX-P0-001). Invoked for the
|
|
148
|
+
// `@Authorized()`-decorated fields applied by applyCortexAuthorizationMap
|
|
149
|
+
// above. SHADOW-FIRST: while `CORTEX_AUTHCHECKER_ENFORCE` is OFF (default),
|
|
150
|
+
// it observes + counts would-deny operations but always allows —
|
|
151
|
+
// byte-identical live behaviour until enforcement is flipped on.
|
|
127
152
|
authChecker: cortex_auth_checker_1.cortexAuthChecker,
|
|
128
153
|
});
|
|
129
154
|
const app = (0, express_1.default)();
|
|
130
155
|
const httpServer = (0, http_1.createServer)(app);
|
|
156
|
+
// Trust the load-balancer proxy chain so `req.ip` resolves the CLIENT address
|
|
157
|
+
// from X-Forwarded-For instead of the proxy hop (audit B01-backend-legacy-02).
|
|
158
|
+
// Without this, every external caller shares one rate-limit bucket per tier,
|
|
159
|
+
// invalidating the shadow signal and making any future enforce flip an outage
|
|
160
|
+
// switch for the whole /graphql surface. `TRUST_PROXY` accepts a hop count
|
|
161
|
+
// (recommended: the EXACT number of proxy hops, so a spoofed X-Forwarded-For
|
|
162
|
+
// cannot mint arbitrary identifiers — audit B01-backend-legacy-12) or an
|
|
163
|
+
// Express trust-proxy string (CIDR / preset). Defaults to 1 hop.
|
|
164
|
+
const rawTrustProxy = ((_a = process.env.TRUST_PROXY) !== null && _a !== void 0 ? _a : '').trim();
|
|
165
|
+
const trustProxy = rawTrustProxy === ''
|
|
166
|
+
? DEFAULT_TRUST_PROXY_HOPS
|
|
167
|
+
: /^\d+$/.test(rawTrustProxy)
|
|
168
|
+
? parseInt(rawTrustProxy, 10)
|
|
169
|
+
: rawTrustProxy;
|
|
170
|
+
app.set('trust proxy', trustProxy);
|
|
131
171
|
// HTTP request metrics — must be mounted early to capture every request.
|
|
132
172
|
// The middleware is a no-op when metrics are disabled.
|
|
133
173
|
app.use(metrics_1.metricsMiddleware);
|
|
134
|
-
// Rate limiting (CORTEX-P0-001). SHADOW-FIRST:
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
174
|
+
// Rate limiting (CORTEX-P0-001). SHADOW-FIRST: while `CORTEX_RATE_LIMIT_ENFORCE`
|
|
175
|
+
// is OFF (default), the limiters observe + count requests that WOULD be blocked
|
|
176
|
+
// but never touch the response, so live behaviour is byte-identical. The
|
|
177
|
+
// GraphQL limiter is mounted INSIDE the /graphql chain after cors() (see below)
|
|
178
|
+
// so an enforce-mode 429 carries CORS headers and browser clients can read the
|
|
179
|
+
// Retry-After instead of seeing an opaque CORS failure
|
|
180
|
+
// (audit B01-backend-legacy-11). Mounted before `/api` auth so limiting
|
|
181
|
+
// precedes the (more expensive) auth work.
|
|
140
182
|
app.use('/api', rate_limiter_1.authRateLimiter);
|
|
141
183
|
app.use('/api', (req, res, next) => (0, auth_1.authMiddleware)(req, res, next));
|
|
142
184
|
// APQ cache: in-memory LRU, gated by `APQ_ENABLED` (default on). The
|
|
@@ -256,7 +298,11 @@ const startServer = async () => {
|
|
|
256
298
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
|
|
257
299
|
maxAge: 86400, // 24h preflight cache
|
|
258
300
|
};
|
|
259
|
-
app.use('/graphql', (0, cors_1.default)(corsOptions),
|
|
301
|
+
app.use('/graphql', (0, cors_1.default)(corsOptions),
|
|
302
|
+
// After cors() so enforce-mode 429s carry CORS headers; before body parsing
|
|
303
|
+
// so limiting stays cheap. OPTIONS preflights are skipped inside the
|
|
304
|
+
// limiter (audit B01-backend-legacy-11).
|
|
305
|
+
rate_limiter_1.graphqlRateLimiter, body_parser_1.default.json(), (0, express4_1.expressMiddleware)(server, {
|
|
260
306
|
context: async ({ req }) => {
|
|
261
307
|
// Ensure we're using the global prisma instance and never disconnecting it between requests
|
|
262
308
|
if (!global.prisma) {
|