@adaptic/backend-legacy 0.0.997 → 0.0.998

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.
@@ -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,CA8B5D,CAAC"}
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). In shadow mode this
20
- * is the signal proving whether flipping `CORTEX_RATE_LIMIT_ENFORCE` to enforce
21
- * would reject legitimate traffic. Labelled by the `limiter` name
22
- * (`graphql`/`auth`) and the request `tier` (`auth`/`anon`).
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
- * Authenticated requests: 1000 requests per 15 minutes (configurable via RATE_LIMIT_MAX)
29
- * Unauthenticated requests: 200 requests per 15 minutes (configurable via RATE_LIMIT_MAX_UNAUTH)
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
- * Authenticated requests: 50 requests per 15 minutes
36
- * Unauthenticated requests: 20 requests per 15 minutes
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;AAItC;;;;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;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,6BAKzC,CAAC;AAkIH;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QArEhB,OAAO,OAAO,QAAQ,QAAQ,YAAY,KAAG,IA+E1D,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAvFb,OAAO,OAAO,QAAQ,QAAQ,YAAY,KAAG,IA+F1D,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":"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"}
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) // GraphQL surface
3
- // app.use('/api', authRateLimiter) // authenticated Express surface
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). In shadow mode this
30
- * is the signal proving whether flipping `CORTEX_RATE_LIMIT_ENFORCE` to enforce
31
- * would reject legitimate traffic. Labelled by the `limiter` name
32
- * (`graphql`/`auth`) and the request `tier` (`auth`/`anon`).
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 (counted in shadow mode without blocking)',
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
- * Checks whether a request carries a valid-looking authentication token.
42
- * Does not verify the token -- only checks for its presence in the
43
- * Authorization header as a Bearer token with three dot-separated parts
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
- function isAuthenticated(req) {
47
- const authHeader = req.headers.authorization || '';
48
- if (!authHeader.startsWith('Bearer ')) {
49
- return false;
50
- }
51
- const token = authHeader.slice(7);
52
- // Only count 3-segment JWT-shaped tokens as "authenticated" for rate-limit
53
- // tiering. Opaque OAuth access tokens (e.g. `ya29.…`) are rejected by the
54
- // verifier downstream treating them as authenticated here would let a
55
- // caller spamming opaque tokens enjoy the higher auth-tier limits.
56
- return token.split('.').length === 3;
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
- * Creates a simple in-memory rate limiter middleware with separate limits
60
- * for authenticated and unauthenticated requests.
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
- * Response headers (when standardHeaders is enabled):
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
- // Clean up expired entries every minute
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
- Object.keys(store).forEach((key) => {
77
- if (store[key].resetTime < now) {
78
- delete store[key];
105
+ for (const [key, entry] of store) {
106
+ if (entry.resetTime < now) {
107
+ store.delete(key);
79
108
  }
80
- });
81
- }, 60000);
109
+ }
110
+ cortexRateLimitStoreKeys.set({ limiter: config.name }, store.size);
111
+ }, STORE_SWEEP_INTERVAL_MS).unref();
82
112
  return (req, res, next) => {
83
- const identifier = req.ip || req.connection.remoteAddress || 'unknown';
84
- const authenticated = isAuthenticated(req);
85
- const tier = authenticated ? 'auth' : 'anon';
86
- const effectiveMax = authenticated
87
- ? config.maxAuthenticated
88
- : config.maxUnauthenticated;
89
- const storeKey = `${identifier}:${tier}`;
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
- if (!store[storeKey] || store[storeKey].resetTime < now) {
92
- store[storeKey] = {
93
- count: 1,
94
- resetTime: now + config.windowMs,
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
- store[storeKey].count += 1;
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
- logger.warn('[cortex-rate-limit] shadow would-block (allowing)', {
111
- limiter: config.name,
112
- identifier,
113
- tier,
114
- count: current.count,
115
- limit: effectiveMax,
116
- resetSeconds,
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
- cortexRateLimitWouldBlockTotal.inc({ limiter: config.name, tier });
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
- * Authenticated requests: 1000 requests per 15 minutes (configurable via RATE_LIMIT_MAX)
143
- * Unauthenticated requests: 200 requests per 15 minutes (configurable via RATE_LIMIT_MAX_UNAUTH)
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
- * Authenticated requests: 50 requests per 15 minutes
160
- * Unauthenticated requests: 20 requests per 15 minutes
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptic/backend-legacy",
3
- "version": "0.0.997",
3
+ "version": "0.0.998",
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). SHADOW-FIRST: only invoked
124
- // for `@Authorized()`-decorated fields and, while `CORTEX_AUTHCHECKER_ENFORCE`
125
- // is OFF (default), it observes + counts would-deny operations but always
126
- // allows byte-identical live behaviour until enforcement is flipped on.
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: mounted on the GraphQL surface
135
- // and the authenticated Express `/api` surface. While `CORTEX_RATE_LIMIT_ENFORCE`
136
- // is OFF (default), they observe + count requests that WOULD be blocked but
137
- // never touch the response, so live behaviour is byte-identical. Mounted
138
- // before `/api` auth so limiting precedes the (more expensive) auth work.
139
- app.use('/graphql', rate_limiter_1.graphqlRateLimiter);
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), body_parser_1.default.json(), (0, express4_1.expressMiddleware)(server, {
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) {