@constructive-io/graphql-server 5.33.3 → 5.33.4

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.
@@ -1,9 +1,10 @@
1
1
  import './types'; // for Request type
2
2
  import { errors } from '@constructive-io/errors';
3
- import { clientIpFrom, ConcurrencyLimiter, DEFAULT_REQUEST_PROTECTION, RateWindow, trustedProxyHops } from '@constructive-io/express-context';
3
+ import { clientIpFrom, ConcurrencyLimiter, DEFAULT_REQUEST_PROTECTION, RateWindow } from '@constructive-io/express-context';
4
4
  import { Logger } from '@pgpmjs/logger';
5
5
  import { respondWithGraphQLError } from '../errors/graphql-response';
6
6
  import { recordRefusal } from '../refusals/recorder';
7
+ import { resolveProxyHops } from './client-ip';
7
8
  const log = new Logger('admission');
8
9
  /**
9
10
  * admission-control — the gate that decides whether a GraphQL request starts.
@@ -44,25 +45,6 @@ const protectionOf = (req) => req.requestProtection ?? DEFAULT_REQUEST_PROTECTIO
44
45
  const routeOf = (req) => `${req.method} ${req.baseUrl}${req.path}`;
45
46
  /** Seconds for a `Retry-After` header — the smallest honest whole number. */
46
47
  const retryAfterSeconds = (ms) => String(Math.max(Math.ceil(ms / 1000), 1));
47
- /**
48
- * How far back through `X-Forwarded-For` to believe.
49
- *
50
- * `req.clientIp` and `req.ip` are both unusable as a limiter key here: this
51
- * server sets `trust proxy` to a predicate that returns true unconditionally,
52
- * and `request-ip` reads the *leftmost* forwarded entry regardless — so either
53
- * one hands a caller a fresh key per request for the cost of a header. The
54
- * fallback to 1 hop exists because the opposite failure is just as bad: behind
55
- * an ingress with no hop count configured, every caller resolves to the
56
- * ingress's address and one abuser throttles the whole tenant.
57
- */
58
- const resolveHops = (req, configured) => {
59
- if (typeof configured === 'number')
60
- return configured;
61
- const fromEnv = trustedProxyHops();
62
- if (fromEnv > 0)
63
- return fromEnv;
64
- return req.app?.get('trust proxy') ? 1 : 0;
65
- };
66
48
  /**
67
49
  * Rate-limit callers per route, then admit at most `maxConcurrentRequests` of
68
50
  * them per database into the handler chain.
@@ -79,7 +61,7 @@ export const createAdmissionControlMiddleware = (options = {}) => {
79
61
  const protection = protectionOf(req);
80
62
  const databaseId = req.databaseId ?? UNKNOWN_DATABASE;
81
63
  // ─── Per-caller rate ────────────────────────────────────────────────────
82
- const hops = resolveHops(req, options.trustedProxyHops);
64
+ const hops = resolveProxyHops(req, options.trustedProxyHops);
83
65
  const ip = clientIpFrom(req, hops);
84
66
  const rateKey = `${databaseId}\u0000${ip}\u0000${routeOf(req)}`;
85
67
  if (!rate.admit(rateKey, protection.rateLimitRpm, protection.rateLimitBurst)) {
@@ -0,0 +1,27 @@
1
+ import './types'; // for Request type
2
+ import { clientIpFrom, trustedProxyHops } from '@constructive-io/express-context';
3
+ /**
4
+ * How far back through `X-Forwarded-For` to believe: an explicit count, then
5
+ * `TRUSTED_PROXY_HOPS` (an explicit 0 included), then 1 when `SERVER_TRUST_PROXY`
6
+ * puts a proxy in front.
7
+ *
8
+ * Express's `trust proxy` predicate and `req.ip` are not used as the answer:
9
+ * the predicate trusts every hop, which would hand back the leftmost entry —
10
+ * the one a caller writes. One ingress means one hop is believed.
11
+ */
12
+ export const resolveProxyHops = (req, configured) => {
13
+ if (typeof configured === 'number')
14
+ return configured;
15
+ const fromEnv = trustedProxyHops();
16
+ if (fromEnv !== null)
17
+ return fromEnv;
18
+ return req.app?.get('trust proxy') ? 1 : 0;
19
+ };
20
+ /**
21
+ * Sets `req.clientIp` — the address `jwt.claims.ip_address`, logs and per-IP
22
+ * limits are keyed on — to the entry our own proxy appended, or the socket peer.
23
+ */
24
+ export const clientIpMiddleware = (req, _res, next) => {
25
+ req.clientIp = clientIpFrom(req, resolveProxyHops(req));
26
+ next();
27
+ };
@@ -1,7 +1,8 @@
1
1
  import '../middleware/types'; // for Request type
2
- import { clientIpFrom, createRecordRefusalsSink, RefusalRecorder, trustedProxyHops } from '@constructive-io/express-context';
2
+ import { clientIpFrom, createRecordRefusalsSink, RefusalRecorder } from '@constructive-io/express-context';
3
3
  import { Logger } from '@pgpmjs/logger';
4
4
  import { getPgPool } from 'pg-cache';
5
+ import { resolveProxyHops } from '../middleware/client-ip';
5
6
  const log = new Logger('refusals');
6
7
  /**
7
8
  * refusals/recorder — the GraphQL lane's `RefusalRecorder`.
@@ -42,19 +43,6 @@ export const getRefusalRecorder = () => installed;
42
43
  export const getRefusalRecorderStats = () => installed?.stats() ?? null;
43
44
  /** The route half of a refusal key — the same shape admission control keys on. */
44
45
  export const routeKeyOf = (req) => `${req.method} ${req.baseUrl ?? ''}${req.path ?? req.url ?? ''}`;
45
- /**
46
- * How far back through `X-Forwarded-For` to believe; mirrors admission
47
- * control's resolution so the refusal source is the same address the limiter
48
- * keyed on.
49
- */
50
- const resolveHops = (req, configured) => {
51
- if (typeof configured === 'number')
52
- return configured;
53
- const fromEnv = trustedProxyHops();
54
- if (fromEnv > 0)
55
- return fromEnv;
56
- return req.app?.get('trust proxy') ? 1 : 0;
57
- };
58
46
  /**
59
47
  * Count one GraphQL-lane refusal. Synchronous; never throws; does nothing
60
48
  * when no recorder is installed.
@@ -70,7 +58,7 @@ export const recordRefusal = (req, reason, opts = {}) => {
70
58
  routeKey: routeKeyOf(req),
71
59
  sourceIp: opts.sourceIp !== undefined
72
60
  ? opts.sourceIp
73
- : clientIpFrom(req, resolveHops(req, opts.trustedProxyHops))
61
+ : clientIpFrom(req, resolveProxyHops(req, opts.trustedProxyHops))
74
62
  };
75
63
  try {
76
64
  recorder.record(refusal);
package/esm/server.js CHANGED
@@ -9,7 +9,6 @@ import express from 'express';
9
9
  import { closeAllCaches, graphileCache } from 'graphile-cache';
10
10
  import graphqlUpload from 'graphql-upload';
11
11
  import { getPgPool } from 'pg-cache';
12
- import requestIp from 'request-ip';
13
12
  import { createAgenticRouter } from './agentic';
14
13
  import { closeDebugDatabasePools } from './diagnostics/debug-db-snapshot';
15
14
  import { startDebugSampler } from './diagnostics/debug-sampler';
@@ -19,6 +18,7 @@ import { createApiMiddleware } from './middleware/api';
19
18
  import { createAuthenticateMiddleware, getBearerToken } from './middleware/auth';
20
19
  // Auth cookie handling is done via AuthCookiePlugin in grafserv
21
20
  import { createCaptchaMiddleware } from './middleware/captcha';
21
+ import { clientIpMiddleware } from './middleware/client-ip';
22
22
  import { parseCookieValue, rejectInvalidSessionCookieHeader, SESSION_COOKIE_NAME } from './middleware/cookie';
23
23
  import { cors } from './middleware/cors';
24
24
  import { errorHandler, notFoundHandler } from './middleware/error-handler';
@@ -135,7 +135,7 @@ class Server {
135
135
  // Rewrite Content-Type after graphql-upload so grafserv accepts the request
136
136
  app.use('/graphql', multipartBridge);
137
137
  app.use(parseDomains());
138
- app.use(requestIp.mw());
138
+ app.use(clientIpMiddleware);
139
139
  app.use(requestIdMiddleware());
140
140
  app.use(requestLogger);
141
141
  app.use(api);
@@ -7,6 +7,7 @@ const express_context_1 = require("@constructive-io/express-context");
7
7
  const logger_1 = require("@pgpmjs/logger");
8
8
  const graphql_response_1 = require("../errors/graphql-response");
9
9
  const recorder_1 = require("../refusals/recorder");
10
+ const client_ip_1 = require("./client-ip");
10
11
  const log = new logger_1.Logger('admission');
11
12
  /**
12
13
  * admission-control — the gate that decides whether a GraphQL request starts.
@@ -47,25 +48,6 @@ const protectionOf = (req) => req.requestProtection ?? express_context_1.DEFAULT
47
48
  const routeOf = (req) => `${req.method} ${req.baseUrl}${req.path}`;
48
49
  /** Seconds for a `Retry-After` header — the smallest honest whole number. */
49
50
  const retryAfterSeconds = (ms) => String(Math.max(Math.ceil(ms / 1000), 1));
50
- /**
51
- * How far back through `X-Forwarded-For` to believe.
52
- *
53
- * `req.clientIp` and `req.ip` are both unusable as a limiter key here: this
54
- * server sets `trust proxy` to a predicate that returns true unconditionally,
55
- * and `request-ip` reads the *leftmost* forwarded entry regardless — so either
56
- * one hands a caller a fresh key per request for the cost of a header. The
57
- * fallback to 1 hop exists because the opposite failure is just as bad: behind
58
- * an ingress with no hop count configured, every caller resolves to the
59
- * ingress's address and one abuser throttles the whole tenant.
60
- */
61
- const resolveHops = (req, configured) => {
62
- if (typeof configured === 'number')
63
- return configured;
64
- const fromEnv = (0, express_context_1.trustedProxyHops)();
65
- if (fromEnv > 0)
66
- return fromEnv;
67
- return req.app?.get('trust proxy') ? 1 : 0;
68
- };
69
51
  /**
70
52
  * Rate-limit callers per route, then admit at most `maxConcurrentRequests` of
71
53
  * them per database into the handler chain.
@@ -82,7 +64,7 @@ const createAdmissionControlMiddleware = (options = {}) => {
82
64
  const protection = protectionOf(req);
83
65
  const databaseId = req.databaseId ?? UNKNOWN_DATABASE;
84
66
  // ─── Per-caller rate ────────────────────────────────────────────────────
85
- const hops = resolveHops(req, options.trustedProxyHops);
67
+ const hops = (0, client_ip_1.resolveProxyHops)(req, options.trustedProxyHops);
86
68
  const ip = (0, express_context_1.clientIpFrom)(req, hops);
87
69
  const rateKey = `${databaseId}\u0000${ip}\u0000${routeOf(req)}`;
88
70
  if (!rate.admit(rateKey, protection.rateLimitRpm, protection.rateLimitBurst)) {
@@ -0,0 +1,17 @@
1
+ import './types';
2
+ import type { NextFunction, Request, Response } from 'express';
3
+ /**
4
+ * How far back through `X-Forwarded-For` to believe: an explicit count, then
5
+ * `TRUSTED_PROXY_HOPS` (an explicit 0 included), then 1 when `SERVER_TRUST_PROXY`
6
+ * puts a proxy in front.
7
+ *
8
+ * Express's `trust proxy` predicate and `req.ip` are not used as the answer:
9
+ * the predicate trusts every hop, which would hand back the leftmost entry —
10
+ * the one a caller writes. One ingress means one hop is believed.
11
+ */
12
+ export declare const resolveProxyHops: (req: Request, configured?: number) => number;
13
+ /**
14
+ * Sets `req.clientIp` — the address `jwt.claims.ip_address`, logs and per-IP
15
+ * limits are keyed on — to the entry our own proxy appended, or the socket peer.
16
+ */
17
+ export declare const clientIpMiddleware: (req: Request, _res: Response, next: NextFunction) => void;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clientIpMiddleware = exports.resolveProxyHops = void 0;
4
+ require("./types"); // for Request type
5
+ const express_context_1 = require("@constructive-io/express-context");
6
+ /**
7
+ * How far back through `X-Forwarded-For` to believe: an explicit count, then
8
+ * `TRUSTED_PROXY_HOPS` (an explicit 0 included), then 1 when `SERVER_TRUST_PROXY`
9
+ * puts a proxy in front.
10
+ *
11
+ * Express's `trust proxy` predicate and `req.ip` are not used as the answer:
12
+ * the predicate trusts every hop, which would hand back the leftmost entry —
13
+ * the one a caller writes. One ingress means one hop is believed.
14
+ */
15
+ const resolveProxyHops = (req, configured) => {
16
+ if (typeof configured === 'number')
17
+ return configured;
18
+ const fromEnv = (0, express_context_1.trustedProxyHops)();
19
+ if (fromEnv !== null)
20
+ return fromEnv;
21
+ return req.app?.get('trust proxy') ? 1 : 0;
22
+ };
23
+ exports.resolveProxyHops = resolveProxyHops;
24
+ /**
25
+ * Sets `req.clientIp` — the address `jwt.claims.ip_address`, logs and per-IP
26
+ * limits are keyed on — to the entry our own proxy appended, or the socket peer.
27
+ */
28
+ const clientIpMiddleware = (req, _res, next) => {
29
+ req.clientIp = (0, express_context_1.clientIpFrom)(req, (0, exports.resolveProxyHops)(req));
30
+ next();
31
+ };
32
+ exports.clientIpMiddleware = clientIpMiddleware;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-server",
3
- "version": "5.33.3",
3
+ "version": "5.33.4",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Constructive GraphQL Server",
6
6
  "main": "index.js",
@@ -43,7 +43,7 @@
43
43
  "@agentic-kit/ollama": "2.16.0",
44
44
  "@constructive-io/csrf": "^0.31.0",
45
45
  "@constructive-io/errors": "^0.25.0",
46
- "@constructive-io/express-context": "^0.35.2",
46
+ "@constructive-io/express-context": "^0.35.3",
47
47
  "@constructive-io/graphql-env": "^3.37.2",
48
48
  "@constructive-io/graphql-types": "^3.36.1",
49
49
  "@constructive-io/llm-env": "^0.16.0",
@@ -72,7 +72,7 @@
72
72
  "graphile-plugin-utils": "^1.14.1",
73
73
  "graphile-presigned-url-plugin": "^1.23.4",
74
74
  "graphile-scoped-introspection": "^0.6.0",
75
- "graphile-settings": "^6.28.2",
75
+ "graphile-settings": "^6.28.3",
76
76
  "graphile-utils": "5.0.3",
77
77
  "graphql": "16.13.0",
78
78
  "graphql-upload": "^13.0.0",
@@ -82,8 +82,7 @@
82
82
  "pg-env": "^1.33.0",
83
83
  "pg-query-context": "^2.33.0",
84
84
  "pg-sql2": "5.0.1",
85
- "postgraphile": "5.1.4",
86
- "request-ip": "^3.3.0"
85
+ "postgraphile": "5.1.4"
87
86
  },
88
87
  "devDependencies": {
89
88
  "@aws-sdk/client-s3": "^3.1052.0",
@@ -92,7 +91,6 @@
92
91
  "@types/express": "^5.0.6",
93
92
  "@types/graphql-upload": "^8.0.12",
94
93
  "@types/pg": "^8.20.4",
95
- "@types/request-ip": "^0.0.41",
96
94
  "@types/supertest": "^7.2.1",
97
95
  "graphile-test": "5.19.0",
98
96
  "makage": "^0.8.0",
@@ -100,5 +98,5 @@
100
98
  "supertest": "^7.2.2",
101
99
  "ts-node": "^10.9.2"
102
100
  },
103
- "gitHead": "f186681da756546c8dad36bd690d525df9b68af3"
101
+ "gitHead": "7ce27b6ff591192fe5eb87e26bb3fb1e538d6505"
104
102
  }
@@ -5,6 +5,7 @@ require("../middleware/types"); // for Request type
5
5
  const express_context_1 = require("@constructive-io/express-context");
6
6
  const logger_1 = require("@pgpmjs/logger");
7
7
  const pg_cache_1 = require("pg-cache");
8
+ const client_ip_1 = require("../middleware/client-ip");
8
9
  const log = new logger_1.Logger('refusals');
9
10
  /**
10
11
  * refusals/recorder — the GraphQL lane's `RefusalRecorder`.
@@ -49,19 +50,6 @@ exports.getRefusalRecorderStats = getRefusalRecorderStats;
49
50
  /** The route half of a refusal key — the same shape admission control keys on. */
50
51
  const routeKeyOf = (req) => `${req.method} ${req.baseUrl ?? ''}${req.path ?? req.url ?? ''}`;
51
52
  exports.routeKeyOf = routeKeyOf;
52
- /**
53
- * How far back through `X-Forwarded-For` to believe; mirrors admission
54
- * control's resolution so the refusal source is the same address the limiter
55
- * keyed on.
56
- */
57
- const resolveHops = (req, configured) => {
58
- if (typeof configured === 'number')
59
- return configured;
60
- const fromEnv = (0, express_context_1.trustedProxyHops)();
61
- if (fromEnv > 0)
62
- return fromEnv;
63
- return req.app?.get('trust proxy') ? 1 : 0;
64
- };
65
53
  /**
66
54
  * Count one GraphQL-lane refusal. Synchronous; never throws; does nothing
67
55
  * when no recorder is installed.
@@ -77,7 +65,7 @@ const recordRefusal = (req, reason, opts = {}) => {
77
65
  routeKey: (0, exports.routeKeyOf)(req),
78
66
  sourceIp: opts.sourceIp !== undefined
79
67
  ? opts.sourceIp
80
- : (0, express_context_1.clientIpFrom)(req, resolveHops(req, opts.trustedProxyHops))
68
+ : (0, express_context_1.clientIpFrom)(req, (0, client_ip_1.resolveProxyHops)(req, opts.trustedProxyHops))
81
69
  };
82
70
  try {
83
71
  recorder.record(refusal);
package/server.js CHANGED
@@ -15,7 +15,6 @@ const express_1 = __importDefault(require("express"));
15
15
  const graphile_cache_1 = require("graphile-cache");
16
16
  const graphql_upload_1 = __importDefault(require("graphql-upload"));
17
17
  const pg_cache_1 = require("pg-cache");
18
- const request_ip_1 = __importDefault(require("request-ip"));
19
18
  const agentic_1 = require("./agentic");
20
19
  const debug_db_snapshot_1 = require("./diagnostics/debug-db-snapshot");
21
20
  const debug_sampler_1 = require("./diagnostics/debug-sampler");
@@ -25,6 +24,7 @@ const api_1 = require("./middleware/api");
25
24
  const auth_1 = require("./middleware/auth");
26
25
  // Auth cookie handling is done via AuthCookiePlugin in grafserv
27
26
  const captcha_1 = require("./middleware/captcha");
27
+ const client_ip_1 = require("./middleware/client-ip");
28
28
  const cookie_1 = require("./middleware/cookie");
29
29
  const cors_1 = require("./middleware/cors");
30
30
  const error_handler_1 = require("./middleware/error-handler");
@@ -142,7 +142,7 @@ class Server {
142
142
  // Rewrite Content-Type after graphql-upload so grafserv accepts the request
143
143
  app.use('/graphql', multipart_bridge_1.multipartBridge);
144
144
  app.use((0, url_domains_1.middleware)());
145
- app.use(request_ip_1.default.mw());
145
+ app.use(client_ip_1.clientIpMiddleware);
146
146
  app.use((0, express_context_1.requestIdMiddleware)());
147
147
  app.use(requestLogger);
148
148
  app.use(api);