@burdenoff/be-sdk 2026.510.79
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/README.md +173 -0
- package/configs/eslint/index.js +492 -0
- package/configs/tsconfig/tsconfig.json +53 -0
- package/dist/EventQueueFactory-6NMSGECR.js +8 -0
- package/dist/EventQueueFactory-6NMSGECR.js.map +1 -0
- package/dist/auth/index.d.ts +66 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/index.js +6 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/captcha/index.d.ts +1 -0
- package/dist/captcha/index.js +4 -0
- package/dist/captcha/index.js.map +1 -0
- package/dist/chunk-2HANKZZM.js +245 -0
- package/dist/chunk-2HANKZZM.js.map +1 -0
- package/dist/chunk-2SD5L2F2.js +816 -0
- package/dist/chunk-2SD5L2F2.js.map +1 -0
- package/dist/chunk-3O2RQOPV.js +1068 -0
- package/dist/chunk-3O2RQOPV.js.map +1 -0
- package/dist/chunk-4B43DZIS.js +560 -0
- package/dist/chunk-4B43DZIS.js.map +1 -0
- package/dist/chunk-4XAEES25.js +2444 -0
- package/dist/chunk-4XAEES25.js.map +1 -0
- package/dist/chunk-4XGGWW36.js +1405 -0
- package/dist/chunk-4XGGWW36.js.map +1 -0
- package/dist/chunk-5LXDPGDH.js +245 -0
- package/dist/chunk-5LXDPGDH.js.map +1 -0
- package/dist/chunk-AI7L22ZJ.js +472 -0
- package/dist/chunk-AI7L22ZJ.js.map +1 -0
- package/dist/chunk-CBZNP6RF.js +286 -0
- package/dist/chunk-CBZNP6RF.js.map +1 -0
- package/dist/chunk-ELLX3K54.js +613 -0
- package/dist/chunk-ELLX3K54.js.map +1 -0
- package/dist/chunk-F5XEAGHQ.js +1875 -0
- package/dist/chunk-F5XEAGHQ.js.map +1 -0
- package/dist/chunk-FNZEQOJY.js +628 -0
- package/dist/chunk-FNZEQOJY.js.map +1 -0
- package/dist/chunk-GTEMQXCA.js +516 -0
- package/dist/chunk-GTEMQXCA.js.map +1 -0
- package/dist/chunk-HIWTWIGQ.js +226 -0
- package/dist/chunk-HIWTWIGQ.js.map +1 -0
- package/dist/chunk-IYE2NTRX.js +764 -0
- package/dist/chunk-IYE2NTRX.js.map +1 -0
- package/dist/chunk-J26YGR2U.js +1484 -0
- package/dist/chunk-J26YGR2U.js.map +1 -0
- package/dist/chunk-JG2FTZPI.js +509 -0
- package/dist/chunk-JG2FTZPI.js.map +1 -0
- package/dist/chunk-JVBTNSPG.js +1551 -0
- package/dist/chunk-JVBTNSPG.js.map +1 -0
- package/dist/chunk-L7ZMN2FJ.js +12 -0
- package/dist/chunk-L7ZMN2FJ.js.map +1 -0
- package/dist/chunk-M5IMU3US.js +473 -0
- package/dist/chunk-M5IMU3US.js.map +1 -0
- package/dist/chunk-MLW2HPS5.js +1267 -0
- package/dist/chunk-MLW2HPS5.js.map +1 -0
- package/dist/chunk-MNCGUYLJ.js +269 -0
- package/dist/chunk-MNCGUYLJ.js.map +1 -0
- package/dist/chunk-MOCUBSTW.js +373 -0
- package/dist/chunk-MOCUBSTW.js.map +1 -0
- package/dist/chunk-NGMEI42S.js +103 -0
- package/dist/chunk-NGMEI42S.js.map +1 -0
- package/dist/chunk-NUB5GQTU.js +270 -0
- package/dist/chunk-NUB5GQTU.js.map +1 -0
- package/dist/chunk-OVSYMNCI.js +145 -0
- package/dist/chunk-OVSYMNCI.js.map +1 -0
- package/dist/chunk-QLVKMWFA.js +94 -0
- package/dist/chunk-QLVKMWFA.js.map +1 -0
- package/dist/chunk-SAG6ZYWQ.js +89 -0
- package/dist/chunk-SAG6ZYWQ.js.map +1 -0
- package/dist/chunk-SAK2JCA7.js +198 -0
- package/dist/chunk-SAK2JCA7.js.map +1 -0
- package/dist/chunk-SBS4S2PJ.js +100 -0
- package/dist/chunk-SBS4S2PJ.js.map +1 -0
- package/dist/chunk-SNEOLWXT.js +148 -0
- package/dist/chunk-SNEOLWXT.js.map +1 -0
- package/dist/chunk-TKBD2ONF.js +564 -0
- package/dist/chunk-TKBD2ONF.js.map +1 -0
- package/dist/chunk-UBXFIIAW.js +127 -0
- package/dist/chunk-UBXFIIAW.js.map +1 -0
- package/dist/chunk-VI4VSNTI.js +57 -0
- package/dist/chunk-VI4VSNTI.js.map +1 -0
- package/dist/chunk-VWCN5PMD.js +268 -0
- package/dist/chunk-VWCN5PMD.js.map +1 -0
- package/dist/chunk-XC2YD6CF.js +913 -0
- package/dist/chunk-XC2YD6CF.js.map +1 -0
- package/dist/chunk-XE4OXN2W.js +10 -0
- package/dist/chunk-XE4OXN2W.js.map +1 -0
- package/dist/chunk-YMZEUTD2.js +110 -0
- package/dist/chunk-YMZEUTD2.js.map +1 -0
- package/dist/chunk-Z4IRLBWO.js +758 -0
- package/dist/chunk-Z4IRLBWO.js.map +1 -0
- package/dist/client/internal-client.d.ts +7 -0
- package/dist/client/internal-client.js +5 -0
- package/dist/client/internal-client.js.map +1 -0
- package/dist/client-CSjIrYKS.d.ts +77 -0
- package/dist/config-UKE2E3BW.js +4 -0
- package/dist/config-UKE2E3BW.js.map +1 -0
- package/dist/crypto/index.d.ts +1 -0
- package/dist/crypto/index.js +4 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/database/index.d.ts +117 -0
- package/dist/database/index.js +7 -0
- package/dist/database/index.js.map +1 -0
- package/dist/dataloader/index.d.ts +1 -0
- package/dist/dataloader/index.js +4 -0
- package/dist/dataloader/index.js.map +1 -0
- package/dist/esm-K6H7EIWK.js +1450 -0
- package/dist/esm-K6H7EIWK.js.map +1 -0
- package/dist/event-queue/index.d.ts +6 -0
- package/dist/event-queue/index.js +10 -0
- package/dist/event-queue/index.js.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.js +9 -0
- package/dist/export/index.js.map +1 -0
- package/dist/feature-flags/index.d.ts +1 -0
- package/dist/feature-flags/index.js +4 -0
- package/dist/feature-flags/index.js.map +1 -0
- package/dist/gateway/index.d.ts +4 -0
- package/dist/gateway/index.js +8 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/security/index.d.ts +2 -0
- package/dist/gateway/security/index.js +5 -0
- package/dist/gateway/security/index.js.map +1 -0
- package/dist/graph/index.d.ts +9 -0
- package/dist/graph/index.js +13 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph-emitter-Bk0FqD53.d.ts +47 -0
- package/dist/graphql/index.d.ts +2 -0
- package/dist/graphql/index.js +7 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/graphql/services/index.d.ts +24 -0
- package/dist/graphql/services/index.js +5 -0
- package/dist/graphql/services/index.js.map +1 -0
- package/dist/graphql-BAVrUgBF.d.ts +22 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +5 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n-DA6IPnYy.d.ts +101 -0
- package/dist/index-4gKQKZ2B.d.ts +71 -0
- package/dist/index-B18rrFru.d.ts +42 -0
- package/dist/index-B70b5mYS.d.ts +33 -0
- package/dist/index-BMq3lFjj.d.ts +247 -0
- package/dist/index-BODgvRtL.d.ts +316 -0
- package/dist/index-BS2idbnr.d.ts +874 -0
- package/dist/index-BV3G48cH.d.ts +124 -0
- package/dist/index-CHbVw5M7.d.ts +73 -0
- package/dist/index-Cq_xNjcv.d.ts +602 -0
- package/dist/index-CrQgidpx.d.ts +77 -0
- package/dist/index-CyVEg86m.d.ts +309 -0
- package/dist/index-DOcBnmMe.d.ts +166 -0
- package/dist/index-DSOpj8DI.d.ts +17 -0
- package/dist/index-DW9q22GH.d.ts +75 -0
- package/dist/index-DlFDFyWD.d.ts +55 -0
- package/dist/index-DoK0bjhH.d.ts +607 -0
- package/dist/index-Dtf6FBaD.d.ts +199 -0
- package/dist/index-KVWGHMbS.d.ts +415 -0
- package/dist/index-PaojfSdd.d.ts +71 -0
- package/dist/index-RU_M5c74.d.ts +163 -0
- package/dist/index-UhkH2eaB.d.ts +192 -0
- package/dist/index-e_18DzAg.d.ts +18 -0
- package/dist/index-jOuh_TKn.d.ts +59 -0
- package/dist/index.d.ts +584 -0
- package/dist/index.js +2626 -0
- package/dist/index.js.map +1 -0
- package/dist/init/index.d.ts +2 -0
- package/dist/init/index.js +12 -0
- package/dist/init/index.js.map +1 -0
- package/dist/instrumentation/index.d.ts +3 -0
- package/dist/instrumentation/index.js +5 -0
- package/dist/instrumentation/index.js.map +1 -0
- package/dist/layer/index.d.ts +2 -0
- package/dist/layer/index.js +6 -0
- package/dist/layer/index.js.map +1 -0
- package/dist/layer-DDip18KY.d.ts +67 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +4 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/notifications/index.d.ts +4 -0
- package/dist/notifications/index.js +7 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/observability/index.d.ts +3 -0
- package/dist/observability/index.js +5 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/pino-logger-RYLGXPHZ.js +5 -0
- package/dist/pino-logger-RYLGXPHZ.js.map +1 -0
- package/dist/prisma/generator/index.d.ts +1 -0
- package/dist/prisma/generator/index.js +228 -0
- package/dist/prisma/generator/index.js.map +1 -0
- package/dist/prisma/index.d.ts +5 -0
- package/dist/prisma/index.js +12 -0
- package/dist/prisma/index.js.map +1 -0
- package/dist/prisma-extension-krXnYnrN.d.ts +30 -0
- package/dist/rbac/index.d.ts +116 -0
- package/dist/rbac/index.js +7 -0
- package/dist/rbac/index.js.map +1 -0
- package/dist/sdk-THO3IWEE.js +136 -0
- package/dist/sdk-THO3IWEE.js.map +1 -0
- package/dist/server/index.d.ts +359 -0
- package/dist/server/index.js +884 -0
- package/dist/server/index.js.map +1 -0
- package/dist/telemetry/index.d.ts +83 -0
- package/dist/telemetry/index.js +209 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/temporal/index.d.ts +148 -0
- package/dist/temporal/index.js +6 -0
- package/dist/temporal/index.js.map +1 -0
- package/dist/tracker/index.d.ts +5 -0
- package/dist/tracker/index.js +9 -0
- package/dist/tracker/index.js.map +1 -0
- package/dist/types-Bo2wkjSP.d.ts +35 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/workspace-guard-LHBNK4TG.js +5 -0
- package/dist/workspace-guard-LHBNK4TG.js.map +1 -0
- package/package.json +241 -0
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
import { logger } from './chunk-CBZNP6RF.js';
|
|
2
|
+
import { __export, __name } from './chunk-XE4OXN2W.js';
|
|
3
|
+
import { GraphQLError, validate, Kind } from 'graphql';
|
|
4
|
+
|
|
5
|
+
// src/gateway/security/index.ts
|
|
6
|
+
var security_exports = {};
|
|
7
|
+
__export(security_exports, {
|
|
8
|
+
DEFAULT_ARMOR_CONFIG: () => DEFAULT_ARMOR_CONFIG,
|
|
9
|
+
DEFAULT_COST_MAP: () => DEFAULT_COST_MAP,
|
|
10
|
+
DEFAULT_TIERS: () => DEFAULT_TIERS,
|
|
11
|
+
MemoryValkey: () => MemoryValkey,
|
|
12
|
+
__resetMetricsForTest: () => __resetMetricsForTest,
|
|
13
|
+
__setSecurityValkeyForTest: () => __setSecurityValkeyForTest,
|
|
14
|
+
buildSecurityPlugins: () => buildSecurityPlugins,
|
|
15
|
+
checkRateLimit: () => checkRateLimit,
|
|
16
|
+
createArmorPlugins: () => createArmorPlugins,
|
|
17
|
+
createOperationTimeoutPlugin: () => createOperationTimeoutPlugin,
|
|
18
|
+
createOtelPlugin: () => createOtelPlugin,
|
|
19
|
+
createQueryComplexityPlugin: () => createQueryComplexityPlugin,
|
|
20
|
+
createRateLimitPlugin: () => createRateLimitPlugin,
|
|
21
|
+
getMetrics: () => getMetrics,
|
|
22
|
+
getSecurityValkey: () => getSecurityValkey,
|
|
23
|
+
initializeMetrics: () => initializeMetrics,
|
|
24
|
+
initializeOtel: () => initializeOtel,
|
|
25
|
+
initializeSecurityValkey: () => initializeSecurityValkey,
|
|
26
|
+
mergeCostMap: () => mergeCostMap,
|
|
27
|
+
resolveActor: () => resolveActor,
|
|
28
|
+
resolveSecurityOptionsFromEnv: () => resolveSecurityOptionsFromEnv,
|
|
29
|
+
resolveTiersFromEnv: () => resolveTiersFromEnv,
|
|
30
|
+
scrapeMetrics: () => scrapeMetrics,
|
|
31
|
+
shutdownOtel: () => shutdownOtel
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// src/gateway/security/actor.ts
|
|
35
|
+
var firstIp = /* @__PURE__ */ __name((header) => {
|
|
36
|
+
if (!header) return void 0;
|
|
37
|
+
return header.split(",")[0]?.trim() || void 0;
|
|
38
|
+
}, "firstIp");
|
|
39
|
+
var classify = /* @__PURE__ */ __name((actorType, actorRole) => {
|
|
40
|
+
if (actorRole === "super_admin") return "super_admin";
|
|
41
|
+
switch (actorType) {
|
|
42
|
+
case "user":
|
|
43
|
+
return "user";
|
|
44
|
+
case "app":
|
|
45
|
+
return "app";
|
|
46
|
+
case "service":
|
|
47
|
+
case "service_account":
|
|
48
|
+
return "service";
|
|
49
|
+
case "super_admin":
|
|
50
|
+
return "super_admin";
|
|
51
|
+
default:
|
|
52
|
+
return "public";
|
|
53
|
+
}
|
|
54
|
+
}, "classify");
|
|
55
|
+
var resolveActor = /* @__PURE__ */ __name((getHeader) => {
|
|
56
|
+
const actorId = getHeader("x-actor-id") ?? void 0;
|
|
57
|
+
const actorType = getHeader("x-actor-type") ?? void 0;
|
|
58
|
+
const actorRole = getHeader("x-actor-role") ?? void 0;
|
|
59
|
+
const workspaceId = getHeader("x-workspace-id") ?? void 0;
|
|
60
|
+
const tenantId = getHeader("x-tenant-id") ?? void 0;
|
|
61
|
+
const orgId = getHeader("x-org-id") ?? void 0;
|
|
62
|
+
const appId = getHeader("x-app-id") ?? void 0;
|
|
63
|
+
const ip = firstIp(getHeader("x-forwarded-for")) || getHeader("x-real-ip") || getHeader("cf-connecting-ip") || void 0;
|
|
64
|
+
const tier = classify(actorType, actorRole);
|
|
65
|
+
let identity;
|
|
66
|
+
switch (tier) {
|
|
67
|
+
case "super_admin":
|
|
68
|
+
identity = "bypass";
|
|
69
|
+
break;
|
|
70
|
+
case "service":
|
|
71
|
+
identity = `svc:${actorId ?? "unknown"}`;
|
|
72
|
+
break;
|
|
73
|
+
case "app":
|
|
74
|
+
identity = `${tenantId ?? "notenant"}:${appId ?? actorId ?? "unknown"}`;
|
|
75
|
+
break;
|
|
76
|
+
case "user":
|
|
77
|
+
identity = `${workspaceId ?? "nows"}:${actorId ?? "unknown"}`;
|
|
78
|
+
break;
|
|
79
|
+
case "public":
|
|
80
|
+
default:
|
|
81
|
+
identity = `ip:${ip ?? "unknown"}`;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
tier,
|
|
85
|
+
identity,
|
|
86
|
+
headers: {
|
|
87
|
+
actorId,
|
|
88
|
+
actorType,
|
|
89
|
+
actorRole,
|
|
90
|
+
workspaceId,
|
|
91
|
+
tenantId,
|
|
92
|
+
orgId,
|
|
93
|
+
appId,
|
|
94
|
+
ip
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}, "resolveActor");
|
|
98
|
+
|
|
99
|
+
// src/gateway/security/cost-map.ts
|
|
100
|
+
var DEFAULT_COST_MAP = {
|
|
101
|
+
// Cross-subgraph search — fanout
|
|
102
|
+
"Query.searchBigConsole": 200,
|
|
103
|
+
"Query.searchWorkspaces": 100,
|
|
104
|
+
"Query.searchTags": 80,
|
|
105
|
+
// Aggregations
|
|
106
|
+
"Query.analyticsReport": 300,
|
|
107
|
+
"Query.usageReport": 250,
|
|
108
|
+
"Query.billingReport": 250,
|
|
109
|
+
"Query.auditLog": 150,
|
|
110
|
+
// Enumeration risk (paginated lists)
|
|
111
|
+
"Query.listUsers": 50,
|
|
112
|
+
"Query.listWorkspaces": 50,
|
|
113
|
+
"Query.listFiles": 40,
|
|
114
|
+
"Query.listTags": 20,
|
|
115
|
+
"Query.listApps": 40,
|
|
116
|
+
// Nested lists
|
|
117
|
+
"Workspace.members": 5,
|
|
118
|
+
"User.workspaces": 10,
|
|
119
|
+
"User.apps": 5,
|
|
120
|
+
"Tag.appliedTo": 5,
|
|
121
|
+
// Introspection — expensive and abuse-prone in prod
|
|
122
|
+
"Query.__schema": 1e3,
|
|
123
|
+
"Query.__type": 500,
|
|
124
|
+
// Heavy writers
|
|
125
|
+
"Mutation.bulkImport": 500,
|
|
126
|
+
"Mutation.webhookTrigger": 100
|
|
127
|
+
};
|
|
128
|
+
var mergeCostMap = /* @__PURE__ */ __name((base, override) => ({
|
|
129
|
+
...base,
|
|
130
|
+
...override
|
|
131
|
+
}), "mergeCostMap");
|
|
132
|
+
|
|
133
|
+
// src/gateway/security/tiers.ts
|
|
134
|
+
var DEFAULT_TIERS = {
|
|
135
|
+
// Public (anonymous) traffic is keyed per-IP. The previous 60+20=80/60s
|
|
136
|
+
// ceiling (1.33 req/s sustained) was tight enough that a single heavy
|
|
137
|
+
// frontend page load (≈15–30 GraphQL queries) plus a NAT-shared second
|
|
138
|
+
// user could blow the bucket on benign traffic. Bumping to 120+60=180/60s
|
|
139
|
+
// (3 req/s sustained) gives 2× headroom while staying ~40× below the
|
|
140
|
+
// ~130 req/s single-pod compute floor measured under realistic load.
|
|
141
|
+
public: {
|
|
142
|
+
points: 120,
|
|
143
|
+
duration: 60,
|
|
144
|
+
burst: 60
|
|
145
|
+
},
|
|
146
|
+
user: {
|
|
147
|
+
points: 600,
|
|
148
|
+
duration: 60,
|
|
149
|
+
burst: 120
|
|
150
|
+
},
|
|
151
|
+
app: {
|
|
152
|
+
points: 3e3,
|
|
153
|
+
duration: 60,
|
|
154
|
+
burst: 500
|
|
155
|
+
},
|
|
156
|
+
service: {
|
|
157
|
+
points: 6e3,
|
|
158
|
+
duration: 60,
|
|
159
|
+
burst: 1e3
|
|
160
|
+
},
|
|
161
|
+
super_admin: {
|
|
162
|
+
points: Number.MAX_SAFE_INTEGER,
|
|
163
|
+
duration: 60,
|
|
164
|
+
burst: Number.MAX_SAFE_INTEGER
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
var positiveInt = /* @__PURE__ */ __name((value, fallback) => {
|
|
168
|
+
if (!value) return fallback;
|
|
169
|
+
const parsed = parseInt(value, 10);
|
|
170
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
171
|
+
}, "positiveInt");
|
|
172
|
+
var resolveTiersFromEnv = /* @__PURE__ */ __name((base = DEFAULT_TIERS) => {
|
|
173
|
+
const out = {
|
|
174
|
+
...base
|
|
175
|
+
};
|
|
176
|
+
Object.keys(base).forEach((tier) => {
|
|
177
|
+
const upper = tier.toUpperCase();
|
|
178
|
+
out[tier] = {
|
|
179
|
+
points: positiveInt(process.env[`GATEWAY_RL_${upper}_POINTS`], base[tier].points),
|
|
180
|
+
duration: positiveInt(process.env[`GATEWAY_RL_${upper}_DURATION`], base[tier].duration),
|
|
181
|
+
burst: positiveInt(process.env[`GATEWAY_RL_${upper}_BURST`], base[tier].burst)
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
return out;
|
|
185
|
+
}, "resolveTiersFromEnv");
|
|
186
|
+
|
|
187
|
+
// src/gateway/security/defaults.ts
|
|
188
|
+
var boolEnv = /* @__PURE__ */ __name((key, fallback) => {
|
|
189
|
+
const v = process.env[key];
|
|
190
|
+
if (v === void 0) return fallback;
|
|
191
|
+
return v === "true" || v === "1";
|
|
192
|
+
}, "boolEnv");
|
|
193
|
+
var intEnv = /* @__PURE__ */ __name((key, fallback) => {
|
|
194
|
+
const v = process.env[key];
|
|
195
|
+
if (!v) return fallback;
|
|
196
|
+
const parsed = parseInt(v, 10);
|
|
197
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
198
|
+
}, "intEnv");
|
|
199
|
+
var strEnv = /* @__PURE__ */ __name((key, fallback) => process.env[key] ?? fallback, "strEnv");
|
|
200
|
+
var DEFAULT_ARMOR_CONFIG = {
|
|
201
|
+
maxAliases: 15,
|
|
202
|
+
maxTokens: 1e3,
|
|
203
|
+
maxDirectives: 50,
|
|
204
|
+
characterLimit: 15e3,
|
|
205
|
+
costLimit: 5e3,
|
|
206
|
+
observe: true
|
|
207
|
+
};
|
|
208
|
+
var resolveSecurityOptionsFromEnv = /* @__PURE__ */ __name((gatewayName, costMapOverride) => {
|
|
209
|
+
return {
|
|
210
|
+
gatewayName,
|
|
211
|
+
enabled: boolEnv("SECURITY_PLUGINS_ENABLED", true),
|
|
212
|
+
armor: {
|
|
213
|
+
maxAliases: intEnv("ARMOR_MAX_ALIASES", DEFAULT_ARMOR_CONFIG.maxAliases),
|
|
214
|
+
maxTokens: intEnv("ARMOR_MAX_TOKENS", DEFAULT_ARMOR_CONFIG.maxTokens),
|
|
215
|
+
maxDirectives: intEnv("ARMOR_MAX_DIRECTIVES", DEFAULT_ARMOR_CONFIG.maxDirectives),
|
|
216
|
+
characterLimit: intEnv("ARMOR_CHARACTER_LIMIT", DEFAULT_ARMOR_CONFIG.characterLimit),
|
|
217
|
+
costLimit: intEnv("ARMOR_COST_LIMIT", DEFAULT_ARMOR_CONFIG.costLimit),
|
|
218
|
+
observe: boolEnv("ARMOR_OBSERVE", true)
|
|
219
|
+
},
|
|
220
|
+
queryComplexity: {
|
|
221
|
+
enabled: boolEnv("QUERY_COMPLEXITY_ENABLED", true),
|
|
222
|
+
maxCost: intEnv("QUERY_COMPLEXITY_MAX", 2e3),
|
|
223
|
+
costMap: costMapOverride ? mergeCostMap(DEFAULT_COST_MAP, costMapOverride) : DEFAULT_COST_MAP,
|
|
224
|
+
observe: boolEnv("QUERY_COMPLEXITY_OBSERVE", true)
|
|
225
|
+
},
|
|
226
|
+
rateLimit: {
|
|
227
|
+
tiers: resolveTiersFromEnv(),
|
|
228
|
+
keyPrefix: strEnv("VALKEY_KEY_PREFIX", gatewayName),
|
|
229
|
+
observeOnly: boolEnv("RATE_LIMIT_OBSERVE_ONLY", true),
|
|
230
|
+
failClosed: boolEnv("RATE_LIMIT_FAIL_CLOSED", false)
|
|
231
|
+
},
|
|
232
|
+
operationTimeout: {
|
|
233
|
+
// Default 15s — shorter than the 30s request-limiter queueTimeout, and
|
|
234
|
+
// aligned with typical client-side timeouts so load-shedding fires at
|
|
235
|
+
// the same edge a saturated client would already give up. Mutations
|
|
236
|
+
// that legitimately take longer can override per-deployment via the
|
|
237
|
+
// OPERATION_TIMEOUT_MS env var.
|
|
238
|
+
timeoutMs: intEnv("OPERATION_TIMEOUT_MS", 15e3)
|
|
239
|
+
},
|
|
240
|
+
otel: {
|
|
241
|
+
enabled: boolEnv("OTEL_ENABLED", true),
|
|
242
|
+
serviceName: strEnv("OTEL_SERVICE_NAME", gatewayName),
|
|
243
|
+
endpoint: strEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://tempo.tooling.internal.burdenoff.com:4318/v1/traces"),
|
|
244
|
+
environment: strEnv("NODE_ENV", "development"),
|
|
245
|
+
version: process.env["APP_VERSION"]
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}, "resolveSecurityOptionsFromEnv");
|
|
249
|
+
|
|
250
|
+
// src/gateway/security/valkey.ts
|
|
251
|
+
var IoRedisAdapter = class IoRedisAdapter2 {
|
|
252
|
+
static {
|
|
253
|
+
__name(this, "IoRedisAdapter");
|
|
254
|
+
}
|
|
255
|
+
client;
|
|
256
|
+
constructor(client) {
|
|
257
|
+
this.client = client;
|
|
258
|
+
}
|
|
259
|
+
get(key) {
|
|
260
|
+
return this.client.get(key);
|
|
261
|
+
}
|
|
262
|
+
set(key, value, options) {
|
|
263
|
+
if (options?.EX) {
|
|
264
|
+
return this.client.set(key, value, "EX", options.EX);
|
|
265
|
+
}
|
|
266
|
+
return this.client.set(key, value);
|
|
267
|
+
}
|
|
268
|
+
incrBy(key, amount) {
|
|
269
|
+
return this.client.incrby(key, amount);
|
|
270
|
+
}
|
|
271
|
+
async expire(key, seconds) {
|
|
272
|
+
const result = await this.client.expire(key, seconds);
|
|
273
|
+
return result === 1;
|
|
274
|
+
}
|
|
275
|
+
ttl(key) {
|
|
276
|
+
return this.client.ttl(key);
|
|
277
|
+
}
|
|
278
|
+
ping() {
|
|
279
|
+
return this.client.ping();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
var buildIoRedisClient = /* @__PURE__ */ __name(async (url) => {
|
|
283
|
+
const spec = "ioredis";
|
|
284
|
+
const mod = await import(
|
|
285
|
+
/* @vite-ignore */
|
|
286
|
+
spec
|
|
287
|
+
);
|
|
288
|
+
const Ctor = mod.default ?? mod.Redis;
|
|
289
|
+
if (!Ctor) {
|
|
290
|
+
throw new Error("ioredis module has no Redis constructor");
|
|
291
|
+
}
|
|
292
|
+
const client = new Ctor(url);
|
|
293
|
+
client.on("error", (err) => {
|
|
294
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
295
|
+
logger.warn("[gateway-security] ioredis error event", {
|
|
296
|
+
error: e.message
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
return new IoRedisAdapter(client);
|
|
300
|
+
}, "buildIoRedisClient");
|
|
301
|
+
var MemoryValkey = class MemoryValkey2 {
|
|
302
|
+
static {
|
|
303
|
+
__name(this, "MemoryValkey");
|
|
304
|
+
}
|
|
305
|
+
store = /* @__PURE__ */ new Map();
|
|
306
|
+
prune(key) {
|
|
307
|
+
const e = this.store.get(key);
|
|
308
|
+
if (!e) return void 0;
|
|
309
|
+
if (e.expiresAt > 0 && e.expiresAt <= Date.now()) {
|
|
310
|
+
this.store.delete(key);
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
return e;
|
|
314
|
+
}
|
|
315
|
+
get(key) {
|
|
316
|
+
const e = this.prune(key);
|
|
317
|
+
return Promise.resolve(e?.value ?? null);
|
|
318
|
+
}
|
|
319
|
+
set(key, value, options) {
|
|
320
|
+
this.store.set(key, {
|
|
321
|
+
value,
|
|
322
|
+
expiresAt: options?.EX ? Date.now() + options.EX * 1e3 : 0
|
|
323
|
+
});
|
|
324
|
+
return Promise.resolve("OK");
|
|
325
|
+
}
|
|
326
|
+
incrBy(key, amount) {
|
|
327
|
+
const e = this.prune(key);
|
|
328
|
+
const next = (e ? parseInt(e.value, 10) || 0 : 0) + amount;
|
|
329
|
+
this.store.set(key, {
|
|
330
|
+
value: String(next),
|
|
331
|
+
expiresAt: e?.expiresAt ?? 0
|
|
332
|
+
});
|
|
333
|
+
return Promise.resolve(next);
|
|
334
|
+
}
|
|
335
|
+
expire(key, seconds) {
|
|
336
|
+
const e = this.store.get(key);
|
|
337
|
+
if (!e) return Promise.resolve(false);
|
|
338
|
+
e.expiresAt = Date.now() + seconds * 1e3;
|
|
339
|
+
return Promise.resolve(true);
|
|
340
|
+
}
|
|
341
|
+
ttl(key) {
|
|
342
|
+
const e = this.prune(key);
|
|
343
|
+
if (!e) return Promise.resolve(-2);
|
|
344
|
+
if (e.expiresAt === 0) return Promise.resolve(-1);
|
|
345
|
+
return Promise.resolve(Math.max(0, Math.ceil((e.expiresAt - Date.now()) / 1e3)));
|
|
346
|
+
}
|
|
347
|
+
ping() {
|
|
348
|
+
return Promise.resolve("PONG");
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
var singleton = null;
|
|
352
|
+
var bootPromise = null;
|
|
353
|
+
var initializeSecurityValkey = /* @__PURE__ */ __name(async (gatewayName) => {
|
|
354
|
+
if (singleton) return singleton;
|
|
355
|
+
if (bootPromise) return bootPromise;
|
|
356
|
+
bootPromise = (async () => {
|
|
357
|
+
const url = process.env["VALKEY_URL"] || process.env["REDIS_URL"];
|
|
358
|
+
const required = process.env["VALKEY_REQUIRED"] === "true";
|
|
359
|
+
const clientKind = process.env["VALKEY_CLIENT"] ?? "ioredis";
|
|
360
|
+
if (!url) {
|
|
361
|
+
const msg = "[gateway-security] VALKEY_URL not set";
|
|
362
|
+
if (required) throw new Error(`${msg} and VALKEY_REQUIRED=true`);
|
|
363
|
+
logger.warn(`${msg} \u2014 using in-memory fallback (not cross-pod safe)`, {
|
|
364
|
+
gatewayName
|
|
365
|
+
});
|
|
366
|
+
singleton = new MemoryValkey();
|
|
367
|
+
return singleton;
|
|
368
|
+
}
|
|
369
|
+
const attempts = [];
|
|
370
|
+
if (clientKind === "ioredis") {
|
|
371
|
+
attempts.push({
|
|
372
|
+
kind: "ioredis",
|
|
373
|
+
factory: /* @__PURE__ */ __name(() => buildIoRedisClient(url), "factory")
|
|
374
|
+
});
|
|
375
|
+
attempts.push({
|
|
376
|
+
kind: "glide (fallback)",
|
|
377
|
+
factory: /* @__PURE__ */ __name(async () => {
|
|
378
|
+
const { getValkeyClient } = await import('./cache/index.js');
|
|
379
|
+
return await getValkeyClient();
|
|
380
|
+
}, "factory")
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
attempts.push({
|
|
384
|
+
kind: "glide",
|
|
385
|
+
factory: /* @__PURE__ */ __name(async () => {
|
|
386
|
+
const { getValkeyClient } = await import('./cache/index.js');
|
|
387
|
+
return await getValkeyClient();
|
|
388
|
+
}, "factory")
|
|
389
|
+
});
|
|
390
|
+
attempts.push({
|
|
391
|
+
kind: "ioredis (fallback)",
|
|
392
|
+
factory: /* @__PURE__ */ __name(() => buildIoRedisClient(url), "factory")
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
let lastError = null;
|
|
396
|
+
for (const attempt of attempts) {
|
|
397
|
+
try {
|
|
398
|
+
const client = await attempt.factory();
|
|
399
|
+
const pong = await client.ping();
|
|
400
|
+
if (pong !== "PONG") {
|
|
401
|
+
throw new Error(`ping returned ${pong}`);
|
|
402
|
+
}
|
|
403
|
+
logger.info("[gateway-security] Valkey connected", {
|
|
404
|
+
gatewayName,
|
|
405
|
+
url,
|
|
406
|
+
client: attempt.kind
|
|
407
|
+
});
|
|
408
|
+
singleton = client;
|
|
409
|
+
return client;
|
|
410
|
+
} catch (err) {
|
|
411
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
412
|
+
logger.warn("[gateway-security] Valkey client init failed, will try next", {
|
|
413
|
+
gatewayName,
|
|
414
|
+
client: attempt.kind,
|
|
415
|
+
error: lastError.message
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (required) {
|
|
420
|
+
logger.error("[gateway-security] Valkey connection failed and required=true", {
|
|
421
|
+
gatewayName,
|
|
422
|
+
error: lastError?.message ?? "all attempts failed"
|
|
423
|
+
});
|
|
424
|
+
throw lastError ?? new Error("Valkey connection failed");
|
|
425
|
+
}
|
|
426
|
+
logger.warn("[gateway-security] All Valkey clients failed \u2014 using in-memory fallback", {
|
|
427
|
+
gatewayName,
|
|
428
|
+
error: lastError?.message ?? "unknown"
|
|
429
|
+
});
|
|
430
|
+
singleton = new MemoryValkey();
|
|
431
|
+
return singleton;
|
|
432
|
+
})();
|
|
433
|
+
return bootPromise;
|
|
434
|
+
}, "initializeSecurityValkey");
|
|
435
|
+
var getSecurityValkey = /* @__PURE__ */ __name(() => {
|
|
436
|
+
if (!singleton) {
|
|
437
|
+
throw new Error("[gateway-security] Valkey not initialized \u2014 call initializeSecurityValkey() first");
|
|
438
|
+
}
|
|
439
|
+
return singleton;
|
|
440
|
+
}, "getSecurityValkey");
|
|
441
|
+
var __setSecurityValkeyForTest = /* @__PURE__ */ __name((client) => {
|
|
442
|
+
singleton = client;
|
|
443
|
+
bootPromise = client ? Promise.resolve(client) : null;
|
|
444
|
+
}, "__setSecurityValkeyForTest");
|
|
445
|
+
|
|
446
|
+
// src/gateway/security/metrics.ts
|
|
447
|
+
var metrics = null;
|
|
448
|
+
var initializing = false;
|
|
449
|
+
var promMod = null;
|
|
450
|
+
var noopCounter = {
|
|
451
|
+
inc: /* @__PURE__ */ __name(() => void 0, "inc")
|
|
452
|
+
};
|
|
453
|
+
var noopMetrics = {
|
|
454
|
+
rateLimit: noopCounter,
|
|
455
|
+
complexityRejected: noopCounter,
|
|
456
|
+
armorRejected: noopCounter,
|
|
457
|
+
valkeyFailover: noopCounter,
|
|
458
|
+
operationTimeout: noopCounter
|
|
459
|
+
};
|
|
460
|
+
var initializeMetrics = /* @__PURE__ */ __name(async () => {
|
|
461
|
+
if (metrics) return metrics;
|
|
462
|
+
if (initializing) {
|
|
463
|
+
return noopMetrics;
|
|
464
|
+
}
|
|
465
|
+
if (process.env["PROMETHEUS_METRICS_ENABLED"] !== "true") {
|
|
466
|
+
logger.info("[gateway-security] Prometheus metrics disabled (PROMETHEUS_METRICS_ENABLED!=true)");
|
|
467
|
+
metrics = noopMetrics;
|
|
468
|
+
return metrics;
|
|
469
|
+
}
|
|
470
|
+
initializing = true;
|
|
471
|
+
try {
|
|
472
|
+
const spec = "prom-client";
|
|
473
|
+
promMod = await import(
|
|
474
|
+
/* @vite-ignore */
|
|
475
|
+
spec
|
|
476
|
+
);
|
|
477
|
+
if (typeof promMod.Counter !== "function") {
|
|
478
|
+
logger.warn("[gateway-security] prom-client has unexpected shape \u2014 metrics disabled");
|
|
479
|
+
metrics = noopMetrics;
|
|
480
|
+
return metrics;
|
|
481
|
+
}
|
|
482
|
+
const { Counter } = promMod;
|
|
483
|
+
metrics = {
|
|
484
|
+
rateLimit: new Counter({
|
|
485
|
+
name: "gateway_security_rate_limit_total",
|
|
486
|
+
help: "Rate-limit plugin decisions by tier + gateway",
|
|
487
|
+
labelNames: [
|
|
488
|
+
"gateway",
|
|
489
|
+
"tier",
|
|
490
|
+
"decision"
|
|
491
|
+
]
|
|
492
|
+
}),
|
|
493
|
+
complexityRejected: new Counter({
|
|
494
|
+
name: "gateway_security_complexity_rejected_total",
|
|
495
|
+
help: "Query complexity plugin rejections",
|
|
496
|
+
labelNames: [
|
|
497
|
+
"gateway",
|
|
498
|
+
"observe"
|
|
499
|
+
]
|
|
500
|
+
}),
|
|
501
|
+
armorRejected: new Counter({
|
|
502
|
+
name: "gateway_security_armor_rejected_total",
|
|
503
|
+
help: "graphql-armor rule rejections",
|
|
504
|
+
labelNames: [
|
|
505
|
+
"gateway",
|
|
506
|
+
"rule",
|
|
507
|
+
"mode"
|
|
508
|
+
]
|
|
509
|
+
}),
|
|
510
|
+
valkeyFailover: new Counter({
|
|
511
|
+
name: "gateway_security_valkey_failover_total",
|
|
512
|
+
help: "Rate-limit plugin fail-open events due to Valkey unreachable",
|
|
513
|
+
labelNames: [
|
|
514
|
+
"gateway"
|
|
515
|
+
]
|
|
516
|
+
}),
|
|
517
|
+
operationTimeout: new Counter({
|
|
518
|
+
name: "gateway_security_operation_timeout_total",
|
|
519
|
+
help: "Operation timeout plugin aborts",
|
|
520
|
+
labelNames: [
|
|
521
|
+
"gateway"
|
|
522
|
+
]
|
|
523
|
+
})
|
|
524
|
+
};
|
|
525
|
+
logger.info("[gateway-security] Prometheus metrics initialized");
|
|
526
|
+
return metrics;
|
|
527
|
+
} catch (err) {
|
|
528
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
529
|
+
logger.warn("[gateway-security] prom-client not installed \u2014 metrics disabled", {
|
|
530
|
+
error: e.message
|
|
531
|
+
});
|
|
532
|
+
metrics = noopMetrics;
|
|
533
|
+
return metrics;
|
|
534
|
+
} finally {
|
|
535
|
+
initializing = false;
|
|
536
|
+
}
|
|
537
|
+
}, "initializeMetrics");
|
|
538
|
+
var getMetrics = /* @__PURE__ */ __name(() => metrics ?? noopMetrics, "getMetrics");
|
|
539
|
+
var scrapeMetrics = /* @__PURE__ */ __name(async () => {
|
|
540
|
+
if (!promMod?.register) return null;
|
|
541
|
+
const body = await promMod.register.metrics();
|
|
542
|
+
return {
|
|
543
|
+
body,
|
|
544
|
+
contentType: promMod.register.contentType
|
|
545
|
+
};
|
|
546
|
+
}, "scrapeMetrics");
|
|
547
|
+
var __resetMetricsForTest = /* @__PURE__ */ __name(() => {
|
|
548
|
+
metrics = null;
|
|
549
|
+
initializing = false;
|
|
550
|
+
promMod = null;
|
|
551
|
+
}, "__resetMetricsForTest");
|
|
552
|
+
var tryImport = /* @__PURE__ */ __name(async (specifier) => {
|
|
553
|
+
try {
|
|
554
|
+
return await import(specifier);
|
|
555
|
+
} catch (err) {
|
|
556
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
557
|
+
logger.warn("[gateway-security] armor package not installed \u2014 rule skipped", {
|
|
558
|
+
specifier,
|
|
559
|
+
error: e.message
|
|
560
|
+
});
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}, "tryImport");
|
|
564
|
+
var pickFactory = /* @__PURE__ */ __name((mod, candidates) => {
|
|
565
|
+
if (!mod) return null;
|
|
566
|
+
for (const name of candidates) {
|
|
567
|
+
const fn = mod[name];
|
|
568
|
+
if (typeof fn === "function") return fn;
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}, "pickFactory");
|
|
572
|
+
var createMaxAliasesRule = /* @__PURE__ */ __name((maxAliases) => {
|
|
573
|
+
return (context) => {
|
|
574
|
+
let aliases = 0;
|
|
575
|
+
return {
|
|
576
|
+
Field(node) {
|
|
577
|
+
if (!node.alias) return;
|
|
578
|
+
aliases += 1;
|
|
579
|
+
if (aliases > maxAliases) {
|
|
580
|
+
context.reportError(new GraphQLError("Too many field aliases.", {
|
|
581
|
+
nodes: node,
|
|
582
|
+
extensions: {
|
|
583
|
+
code: "ARMOR_MAX_ALIASES"
|
|
584
|
+
}
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
};
|
|
590
|
+
}, "createMaxAliasesRule");
|
|
591
|
+
var createMaxDirectivesRule = /* @__PURE__ */ __name((maxDirectives) => {
|
|
592
|
+
return (context) => {
|
|
593
|
+
let directives = 0;
|
|
594
|
+
return {
|
|
595
|
+
Directive(node) {
|
|
596
|
+
directives += 1;
|
|
597
|
+
if (directives > maxDirectives) {
|
|
598
|
+
context.reportError(new GraphQLError("Too many directives.", {
|
|
599
|
+
nodes: node,
|
|
600
|
+
extensions: {
|
|
601
|
+
code: "ARMOR_MAX_DIRECTIVES"
|
|
602
|
+
}
|
|
603
|
+
}));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
}, "createMaxDirectivesRule");
|
|
609
|
+
var isLean = /* @__PURE__ */ __name(() => {
|
|
610
|
+
const v = process.env["ARMOR_LEAN"];
|
|
611
|
+
return v === "true" || v === "1";
|
|
612
|
+
}, "isLean");
|
|
613
|
+
var loadRules = /* @__PURE__ */ __name(async (cfg) => {
|
|
614
|
+
const lean = isLean();
|
|
615
|
+
const [aliasesMod, directivesMod, costMod, blockSuggestionsMod] = await Promise.all([
|
|
616
|
+
lean ? Promise.resolve(null) : tryImport("@escape.tech/graphql-armor-max-aliases"),
|
|
617
|
+
lean ? Promise.resolve(null) : tryImport("@escape.tech/graphql-armor-max-directives"),
|
|
618
|
+
tryImport("@escape.tech/graphql-armor-cost-limit"),
|
|
619
|
+
lean ? Promise.resolve(null) : tryImport("@escape.tech/graphql-armor-block-field-suggestions")
|
|
620
|
+
]);
|
|
621
|
+
const loaded = {};
|
|
622
|
+
const maxAliasesFactory = pickFactory(aliasesMod, [
|
|
623
|
+
"maxAliasesRule"
|
|
624
|
+
]);
|
|
625
|
+
loaded.maxAliasesRule = maxAliasesFactory ? maxAliasesFactory({
|
|
626
|
+
n: cfg.maxAliases,
|
|
627
|
+
exposeLimits: cfg.observe
|
|
628
|
+
}) : createMaxAliasesRule(cfg.maxAliases);
|
|
629
|
+
const maxDirectivesFactory = pickFactory(directivesMod, [
|
|
630
|
+
"maxDirectivesRule"
|
|
631
|
+
]);
|
|
632
|
+
loaded.maxDirectivesRule = maxDirectivesFactory ? maxDirectivesFactory({
|
|
633
|
+
n: cfg.maxDirectives,
|
|
634
|
+
exposeLimits: cfg.observe
|
|
635
|
+
}) : createMaxDirectivesRule(cfg.maxDirectives);
|
|
636
|
+
const costLimitFactory = pickFactory(costMod, [
|
|
637
|
+
"costLimitRule"
|
|
638
|
+
]);
|
|
639
|
+
if (costLimitFactory) {
|
|
640
|
+
loaded.costLimitRule = costLimitFactory({
|
|
641
|
+
maxCost: cfg.costLimit,
|
|
642
|
+
objectCost: 2,
|
|
643
|
+
scalarCost: 1,
|
|
644
|
+
depthCostFactor: 1.5,
|
|
645
|
+
ignoreIntrospection: true,
|
|
646
|
+
exposeLimits: cfg.observe
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
const blockSuggestionsFactory = pickFactory(blockSuggestionsMod, [
|
|
650
|
+
"blockFieldSuggestionsPlugin",
|
|
651
|
+
"default"
|
|
652
|
+
]);
|
|
653
|
+
if (blockSuggestionsFactory) {
|
|
654
|
+
loaded.blockSuggestionsPlugin = blockSuggestionsFactory({});
|
|
655
|
+
}
|
|
656
|
+
if (lean) {
|
|
657
|
+
logger.info("[gateway-security] ARMOR_LEAN enabled \u2014 running only cost-limit + character-limit", {});
|
|
658
|
+
}
|
|
659
|
+
return loaded;
|
|
660
|
+
}, "loadRules");
|
|
661
|
+
var createArmorPlugins = /* @__PURE__ */ __name(async (cfg) => {
|
|
662
|
+
const rules = await loadRules(cfg);
|
|
663
|
+
const metrics2 = getMetrics();
|
|
664
|
+
const enabledRules = [];
|
|
665
|
+
if (rules.maxAliasesRule) {
|
|
666
|
+
enabledRules.push({
|
|
667
|
+
name: "max-aliases",
|
|
668
|
+
rule: rules.maxAliasesRule
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
if (rules.maxDirectivesRule) {
|
|
672
|
+
enabledRules.push({
|
|
673
|
+
name: "max-directives",
|
|
674
|
+
rule: rules.maxDirectivesRule
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
if (rules.costLimitRule) {
|
|
678
|
+
enabledRules.push({
|
|
679
|
+
name: "cost-limit",
|
|
680
|
+
rule: rules.costLimitRule
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
const armorValidationPlugin = {
|
|
684
|
+
onParse({ params }) {
|
|
685
|
+
const source = typeof params.source === "string" ? params.source : params.source?.body;
|
|
686
|
+
if (typeof source === "string" && source.length > cfg.characterLimit) {
|
|
687
|
+
if (cfg.observe) {
|
|
688
|
+
logger.warn("[gateway-security] armor character-limit exceeded (observe)", {
|
|
689
|
+
length: source.length,
|
|
690
|
+
limit: cfg.characterLimit
|
|
691
|
+
});
|
|
692
|
+
metrics2.armorRejected.inc({
|
|
693
|
+
rule: "character-limit",
|
|
694
|
+
mode: "observe"
|
|
695
|
+
}, 1);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
metrics2.armorRejected.inc({
|
|
699
|
+
rule: "character-limit",
|
|
700
|
+
mode: "enforce"
|
|
701
|
+
}, 1);
|
|
702
|
+
throw new GraphQLError("Syntax Error: Query exceeds maximum character limit.", {
|
|
703
|
+
extensions: {
|
|
704
|
+
code: "CHARACTER_LIMIT_EXCEEDED",
|
|
705
|
+
http: {
|
|
706
|
+
status: 400
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
onValidate({ params, setResult }) {
|
|
713
|
+
if (enabledRules.length === 0) return;
|
|
714
|
+
const armorErrors = [];
|
|
715
|
+
for (const { name, rule } of enabledRules) {
|
|
716
|
+
try {
|
|
717
|
+
const errs = validate(params.schema, params.documentAST, [
|
|
718
|
+
rule
|
|
719
|
+
]);
|
|
720
|
+
armorErrors.push(...errs);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
723
|
+
const wrapped = new GraphQLError(message, {
|
|
724
|
+
originalError: err instanceof Error ? err : void 0,
|
|
725
|
+
extensions: {
|
|
726
|
+
code: "ARMOR_VIOLATION",
|
|
727
|
+
rule: name
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
armorErrors.push(wrapped);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (armorErrors.length === 0) return;
|
|
734
|
+
for (const err of armorErrors) {
|
|
735
|
+
logger.warn("[gateway-security] armor rule triggered", {
|
|
736
|
+
message: err.message,
|
|
737
|
+
observe: cfg.observe
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
metrics2.armorRejected.inc({
|
|
741
|
+
rule: "armor",
|
|
742
|
+
mode: cfg.observe ? "observe" : "enforce"
|
|
743
|
+
}, armorErrors.length);
|
|
744
|
+
if (cfg.observe) return;
|
|
745
|
+
setResult(armorErrors);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
const plugins = [
|
|
749
|
+
armorValidationPlugin
|
|
750
|
+
];
|
|
751
|
+
if (rules.blockSuggestionsPlugin) {
|
|
752
|
+
plugins.push(rules.blockSuggestionsPlugin);
|
|
753
|
+
}
|
|
754
|
+
return plugins;
|
|
755
|
+
}, "createArmorPlugins");
|
|
756
|
+
var createTimeoutHandle = /* @__PURE__ */ __name((timeoutMs, error) => {
|
|
757
|
+
let timerRef;
|
|
758
|
+
const promise = new Promise((_, reject) => {
|
|
759
|
+
timerRef = setTimeout(() => reject(error), timeoutMs);
|
|
760
|
+
});
|
|
761
|
+
return {
|
|
762
|
+
timer: timerRef,
|
|
763
|
+
promise
|
|
764
|
+
};
|
|
765
|
+
}, "createTimeoutHandle");
|
|
766
|
+
var createOperationTimeoutPlugin = /* @__PURE__ */ __name((deps) => {
|
|
767
|
+
const timeoutError = new GraphQLError("Operation timeout", {
|
|
768
|
+
extensions: {
|
|
769
|
+
code: "OPERATION_TIMEOUT",
|
|
770
|
+
timeoutMs: deps.config.timeoutMs
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
return {
|
|
774
|
+
onExecute: /* @__PURE__ */ __name(({ executeFn, setExecuteFn }) => {
|
|
775
|
+
setExecuteFn(async (args) => {
|
|
776
|
+
const handle = createTimeoutHandle(deps.config.timeoutMs, timeoutError);
|
|
777
|
+
try {
|
|
778
|
+
return await Promise.race([
|
|
779
|
+
executeFn(args),
|
|
780
|
+
handle.promise
|
|
781
|
+
]);
|
|
782
|
+
} finally {
|
|
783
|
+
clearTimeout(handle.timer);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
}, "onExecute")
|
|
787
|
+
};
|
|
788
|
+
}, "createOperationTimeoutPlugin");
|
|
789
|
+
|
|
790
|
+
// src/gateway/security/plugins/otel.ts
|
|
791
|
+
var shutdownHook = null;
|
|
792
|
+
var buildResource = /* @__PURE__ */ __name((resMod, attrs) => {
|
|
793
|
+
if (typeof resMod.resourceFromAttributes === "function") {
|
|
794
|
+
return resMod.resourceFromAttributes(attrs);
|
|
795
|
+
}
|
|
796
|
+
if (typeof resMod.Resource === "function") {
|
|
797
|
+
return new resMod.Resource(attrs);
|
|
798
|
+
}
|
|
799
|
+
throw new Error("@opentelemetry/resources module exposes neither Resource nor resourceFromAttributes");
|
|
800
|
+
}, "buildResource");
|
|
801
|
+
var resolveSemanticKeys = /* @__PURE__ */ __name((semcon) => {
|
|
802
|
+
if (semcon.SemanticResourceAttributes) {
|
|
803
|
+
return {
|
|
804
|
+
serviceName: semcon.SemanticResourceAttributes.SERVICE_NAME,
|
|
805
|
+
deploymentEnv: semcon.SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT,
|
|
806
|
+
serviceVersion: semcon.SemanticResourceAttributes.SERVICE_VERSION
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
serviceName: semcon.ATTR_SERVICE_NAME ?? "service.name",
|
|
811
|
+
deploymentEnv: semcon.ATTR_DEPLOYMENT_ENVIRONMENT_NAME ?? semcon.ATTR_DEPLOYMENT_ENVIRONMENT ?? "deployment.environment",
|
|
812
|
+
serviceVersion: semcon.ATTR_SERVICE_VERSION ?? "service.version"
|
|
813
|
+
};
|
|
814
|
+
}, "resolveSemanticKeys");
|
|
815
|
+
var loadOtelPeerDeps = /* @__PURE__ */ __name(async () => {
|
|
816
|
+
const sdkSpec = "@opentelemetry/sdk-node";
|
|
817
|
+
const otlpSpec = "@opentelemetry/exporter-trace-otlp-http";
|
|
818
|
+
const resSpec = "@opentelemetry/resources";
|
|
819
|
+
const semSpec = "@opentelemetry/semantic-conventions";
|
|
820
|
+
const modules = await Promise.all([
|
|
821
|
+
import(
|
|
822
|
+
/* @vite-ignore */
|
|
823
|
+
sdkSpec
|
|
824
|
+
),
|
|
825
|
+
import(
|
|
826
|
+
/* @vite-ignore */
|
|
827
|
+
otlpSpec
|
|
828
|
+
),
|
|
829
|
+
import(
|
|
830
|
+
/* @vite-ignore */
|
|
831
|
+
resSpec
|
|
832
|
+
),
|
|
833
|
+
import(
|
|
834
|
+
/* @vite-ignore */
|
|
835
|
+
semSpec
|
|
836
|
+
)
|
|
837
|
+
]);
|
|
838
|
+
const [sdkMod, otlpMod, resMod, semcon] = modules;
|
|
839
|
+
return {
|
|
840
|
+
NodeSDK: sdkMod.NodeSDK,
|
|
841
|
+
OTLPTraceExporter: otlpMod.OTLPTraceExporter,
|
|
842
|
+
resMod,
|
|
843
|
+
semcon
|
|
844
|
+
};
|
|
845
|
+
}, "loadOtelPeerDeps");
|
|
846
|
+
var initializeOtel = /* @__PURE__ */ __name(async (cfg) => {
|
|
847
|
+
if (!cfg.enabled) {
|
|
848
|
+
logger.info("[gateway-security] OTel disabled by config");
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (shutdownHook) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
const { NodeSDK, OTLPTraceExporter, resMod, semcon } = await loadOtelPeerDeps();
|
|
856
|
+
const keys = resolveSemanticKeys(semcon);
|
|
857
|
+
const resourceAttributes = {
|
|
858
|
+
[keys.serviceName]: cfg.serviceName,
|
|
859
|
+
[keys.deploymentEnv]: cfg.environment
|
|
860
|
+
};
|
|
861
|
+
if (cfg.version) {
|
|
862
|
+
resourceAttributes[keys.serviceVersion] = cfg.version;
|
|
863
|
+
}
|
|
864
|
+
const sdk = new NodeSDK({
|
|
865
|
+
resource: buildResource(resMod, resourceAttributes),
|
|
866
|
+
traceExporter: new OTLPTraceExporter({
|
|
867
|
+
url: cfg.endpoint
|
|
868
|
+
})
|
|
869
|
+
});
|
|
870
|
+
sdk.start();
|
|
871
|
+
shutdownHook = /* @__PURE__ */ __name(() => sdk.shutdown(), "shutdownHook");
|
|
872
|
+
logger.info("[gateway-security] OTel initialized", {
|
|
873
|
+
serviceName: cfg.serviceName,
|
|
874
|
+
endpoint: cfg.endpoint
|
|
875
|
+
});
|
|
876
|
+
} catch (err) {
|
|
877
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
878
|
+
logger.warn("[gateway-security] OTel init failed \u2014 continuing without tracing", {
|
|
879
|
+
error: e.message
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}, "initializeOtel");
|
|
883
|
+
var shutdownOtel = /* @__PURE__ */ __name(async () => {
|
|
884
|
+
if (shutdownHook) {
|
|
885
|
+
try {
|
|
886
|
+
await shutdownHook();
|
|
887
|
+
} catch (err) {
|
|
888
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
889
|
+
logger.warn("[gateway-security] OTel shutdown error", {
|
|
890
|
+
error: e.message
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
shutdownHook = null;
|
|
894
|
+
}
|
|
895
|
+
}, "shutdownOtel");
|
|
896
|
+
var createOtelPlugin = /* @__PURE__ */ __name(async (cfg) => {
|
|
897
|
+
if (!cfg.enabled) return {};
|
|
898
|
+
try {
|
|
899
|
+
const spec = "@envelop/opentelemetry";
|
|
900
|
+
const mod = await import(
|
|
901
|
+
/* @vite-ignore */
|
|
902
|
+
spec
|
|
903
|
+
);
|
|
904
|
+
if (typeof mod.useOpenTelemetry !== "function") {
|
|
905
|
+
logger.warn("[gateway-security] @envelop/opentelemetry has unexpected shape \u2014 plugin skipped");
|
|
906
|
+
return {};
|
|
907
|
+
}
|
|
908
|
+
return mod.useOpenTelemetry({
|
|
909
|
+
spans: {
|
|
910
|
+
execute: true,
|
|
911
|
+
parse: true,
|
|
912
|
+
validate: true,
|
|
913
|
+
subscribe: true
|
|
914
|
+
},
|
|
915
|
+
resolvers: false
|
|
916
|
+
});
|
|
917
|
+
} catch (err) {
|
|
918
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
919
|
+
logger.warn("[gateway-security] @envelop/opentelemetry not installed \u2014 plugin skipped", {
|
|
920
|
+
error: e.message
|
|
921
|
+
});
|
|
922
|
+
return {};
|
|
923
|
+
}
|
|
924
|
+
}, "createOtelPlugin");
|
|
925
|
+
var PAGINATION_ARGUMENTS = /* @__PURE__ */ new Set([
|
|
926
|
+
"first",
|
|
927
|
+
"limit",
|
|
928
|
+
"pageSize"
|
|
929
|
+
]);
|
|
930
|
+
var MAX_LIST_MULTIPLIER = 100;
|
|
931
|
+
var MAX_FAILURE_LOG_VARIANTS = 5;
|
|
932
|
+
var complexityFailureMessages = /* @__PURE__ */ new Set();
|
|
933
|
+
var isRecord = /* @__PURE__ */ __name((value) => typeof value === "object" && value !== null, "isRecord");
|
|
934
|
+
var unwrapNamedType = /* @__PURE__ */ __name((type) => {
|
|
935
|
+
let current = type;
|
|
936
|
+
while (isRecord(current) && current["ofType"] !== void 0) {
|
|
937
|
+
current = current["ofType"];
|
|
938
|
+
}
|
|
939
|
+
return isRecord(current) ? current : null;
|
|
940
|
+
}, "unwrapNamedType");
|
|
941
|
+
var getSchemaLike = /* @__PURE__ */ __name((schema) => schema, "getSchemaLike");
|
|
942
|
+
var getOperationRootType = /* @__PURE__ */ __name((schema, operation) => {
|
|
943
|
+
const schemaLike = getSchemaLike(schema);
|
|
944
|
+
if (operation.operation === "mutation") return schemaLike.getMutationType?.() ?? null;
|
|
945
|
+
if (operation.operation === "subscription") return schemaLike.getSubscriptionType?.() ?? null;
|
|
946
|
+
return schemaLike.getQueryType?.() ?? null;
|
|
947
|
+
}, "getOperationRootType");
|
|
948
|
+
var getTypeByName = /* @__PURE__ */ __name((schema, name) => {
|
|
949
|
+
return getSchemaLike(schema).getType?.(name) ?? null;
|
|
950
|
+
}, "getTypeByName");
|
|
951
|
+
var getField = /* @__PURE__ */ __name((parentType, fieldName) => {
|
|
952
|
+
return parentType.getFields?.()[fieldName];
|
|
953
|
+
}, "getField");
|
|
954
|
+
var getCostMapValue = /* @__PURE__ */ __name((costMap, parentType, fieldName) => {
|
|
955
|
+
if (!parentType.name) return 1;
|
|
956
|
+
return costMap[`${parentType.name}.${fieldName}`] ?? 1;
|
|
957
|
+
}, "getCostMapValue");
|
|
958
|
+
var numericValueFromAst = /* @__PURE__ */ __name((value) => {
|
|
959
|
+
if (value.kind === Kind.INT || value.kind === Kind.FLOAT) {
|
|
960
|
+
const parsed = Number(value.value);
|
|
961
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
962
|
+
}
|
|
963
|
+
if (value.kind === Kind.STRING) {
|
|
964
|
+
const parsed = Number(value.value);
|
|
965
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
966
|
+
}
|
|
967
|
+
return void 0;
|
|
968
|
+
}, "numericValueFromAst");
|
|
969
|
+
var listMultiplierFromArgs = /* @__PURE__ */ __name((args) => {
|
|
970
|
+
if (!args) return 1;
|
|
971
|
+
for (const arg of args) {
|
|
972
|
+
if (!PAGINATION_ARGUMENTS.has(arg.name.value)) continue;
|
|
973
|
+
const raw = numericValueFromAst(arg.value);
|
|
974
|
+
if (typeof raw !== "number") return 1;
|
|
975
|
+
return Math.min(Math.max(1, Math.trunc(raw)), MAX_LIST_MULTIPLIER);
|
|
976
|
+
}
|
|
977
|
+
return 1;
|
|
978
|
+
}, "listMultiplierFromArgs");
|
|
979
|
+
var calculateFieldComplexity = /* @__PURE__ */ __name((schema, fieldNode, parentType, fragments, costMap, activeFragments) => {
|
|
980
|
+
const fieldName = fieldNode.name.value;
|
|
981
|
+
const field = getField(parentType, fieldName);
|
|
982
|
+
const baseCost = getCostMapValue(costMap, parentType, fieldName);
|
|
983
|
+
const namedFieldType = unwrapNamedType(field?.type);
|
|
984
|
+
const childCost = fieldNode.selectionSet && namedFieldType ? calculateSelectionSetComplexity(schema, fieldNode.selectionSet, namedFieldType, fragments, costMap, activeFragments) : 0;
|
|
985
|
+
return (baseCost + childCost) * listMultiplierFromArgs(fieldNode.arguments);
|
|
986
|
+
}, "calculateFieldComplexity");
|
|
987
|
+
var calculateSelectionSetComplexity = /* @__PURE__ */ __name((schema, selectionSet, parentType, fragments, costMap, activeFragments) => {
|
|
988
|
+
let total = 0;
|
|
989
|
+
for (const selection of selectionSet.selections) {
|
|
990
|
+
total += calculateSelectionComplexity(schema, selection, parentType, fragments, costMap, activeFragments);
|
|
991
|
+
}
|
|
992
|
+
return total;
|
|
993
|
+
}, "calculateSelectionSetComplexity");
|
|
994
|
+
var calculateSelectionComplexity = /* @__PURE__ */ __name((schema, selection, parentType, fragments, costMap, activeFragments) => {
|
|
995
|
+
if (selection.kind === Kind.FIELD) {
|
|
996
|
+
return calculateFieldComplexity(schema, selection, parentType, fragments, costMap, activeFragments);
|
|
997
|
+
}
|
|
998
|
+
if (selection.kind === Kind.INLINE_FRAGMENT) {
|
|
999
|
+
const fragmentParent2 = selection.typeCondition ? getTypeByName(schema, selection.typeCondition.name.value) ?? parentType : parentType;
|
|
1000
|
+
return calculateSelectionSetComplexity(schema, selection.selectionSet, fragmentParent2, fragments, costMap, activeFragments);
|
|
1001
|
+
}
|
|
1002
|
+
const fragmentName = selection.name.value;
|
|
1003
|
+
if (activeFragments.has(fragmentName)) return 0;
|
|
1004
|
+
const fragment = fragments.get(fragmentName);
|
|
1005
|
+
if (!fragment) return 0;
|
|
1006
|
+
activeFragments.add(fragmentName);
|
|
1007
|
+
const fragmentParent = getTypeByName(schema, fragment.typeCondition.name.value) ?? parentType;
|
|
1008
|
+
const complexity = calculateSelectionSetComplexity(schema, fragment.selectionSet, fragmentParent, fragments, costMap, activeFragments);
|
|
1009
|
+
activeFragments.delete(fragmentName);
|
|
1010
|
+
return complexity;
|
|
1011
|
+
}, "calculateSelectionComplexity");
|
|
1012
|
+
var calculateDocumentComplexity = /* @__PURE__ */ __name((schema, documentAST, costMap) => {
|
|
1013
|
+
const fragments = /* @__PURE__ */ new Map();
|
|
1014
|
+
const operations = [];
|
|
1015
|
+
for (const definition of documentAST.definitions) {
|
|
1016
|
+
if (!isRecord(definition) || typeof definition["kind"] !== "string") continue;
|
|
1017
|
+
if (definition["kind"] === Kind.FRAGMENT_DEFINITION) {
|
|
1018
|
+
const fragment = definition;
|
|
1019
|
+
fragments.set(fragment.name.value, fragment);
|
|
1020
|
+
} else if (definition["kind"] === Kind.OPERATION_DEFINITION) {
|
|
1021
|
+
operations.push(definition);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
let maxComplexity = 0;
|
|
1025
|
+
for (const operation of operations) {
|
|
1026
|
+
const rootType = getOperationRootType(schema, operation);
|
|
1027
|
+
if (!rootType) continue;
|
|
1028
|
+
const complexity = calculateSelectionSetComplexity(schema, operation.selectionSet, rootType, fragments, costMap, /* @__PURE__ */ new Set());
|
|
1029
|
+
maxComplexity = Math.max(maxComplexity, complexity);
|
|
1030
|
+
}
|
|
1031
|
+
return maxComplexity;
|
|
1032
|
+
}, "calculateDocumentComplexity");
|
|
1033
|
+
var logComplexityFailure = /* @__PURE__ */ __name((error) => {
|
|
1034
|
+
const seen = complexityFailureMessages.has(error.message);
|
|
1035
|
+
if (!seen && complexityFailureMessages.size < MAX_FAILURE_LOG_VARIANTS) {
|
|
1036
|
+
complexityFailureMessages.add(error.message);
|
|
1037
|
+
logger.warn("[gateway-security] complexity scoring failed \u2014 fail-open", {
|
|
1038
|
+
error: error.message,
|
|
1039
|
+
suppressedRepeats: true
|
|
1040
|
+
});
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
logger.debug("[gateway-security] repeated complexity scoring failure suppressed", {
|
|
1044
|
+
error: error.message
|
|
1045
|
+
});
|
|
1046
|
+
}, "logComplexityFailure");
|
|
1047
|
+
var createQueryComplexityPlugin = /* @__PURE__ */ __name((deps) => {
|
|
1048
|
+
return {
|
|
1049
|
+
onValidate: /* @__PURE__ */ __name(({ params, setResult }) => {
|
|
1050
|
+
try {
|
|
1051
|
+
const complexity = calculateDocumentComplexity(params.schema, params.documentAST, deps.config.costMap);
|
|
1052
|
+
if (complexity > deps.config.maxCost) {
|
|
1053
|
+
getMetrics().complexityRejected.inc({
|
|
1054
|
+
gateway: "unknown",
|
|
1055
|
+
observe: deps.config.observe ? "true" : "false"
|
|
1056
|
+
});
|
|
1057
|
+
if (deps.config.observe) {
|
|
1058
|
+
logger.info("[gateway-security] complexity would_reject (observe)", {
|
|
1059
|
+
complexity,
|
|
1060
|
+
maxCost: deps.config.maxCost
|
|
1061
|
+
});
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
setResult([
|
|
1065
|
+
new GraphQLError(`Query too expensive (cost ${complexity} > limit ${deps.config.maxCost})`, {
|
|
1066
|
+
extensions: {
|
|
1067
|
+
code: "COMPLEXITY_TOO_HIGH",
|
|
1068
|
+
cost: complexity,
|
|
1069
|
+
maxCost: deps.config.maxCost
|
|
1070
|
+
}
|
|
1071
|
+
})
|
|
1072
|
+
]);
|
|
1073
|
+
}
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1076
|
+
logComplexityFailure(e);
|
|
1077
|
+
}
|
|
1078
|
+
}, "onValidate")
|
|
1079
|
+
};
|
|
1080
|
+
}, "createQueryComplexityPlugin");
|
|
1081
|
+
|
|
1082
|
+
// src/gateway/security/plugins/rate-limit.ts
|
|
1083
|
+
var keyFor = /* @__PURE__ */ __name((prefix, tier, identity) => `rl:${prefix}:${tier}:${identity}`, "keyFor");
|
|
1084
|
+
var stamp = /* @__PURE__ */ __name(() => Date.now(), "stamp");
|
|
1085
|
+
var checkRateLimit = /* @__PURE__ */ __name(async (deps, actor) => {
|
|
1086
|
+
const tier = deps.config.tiers[actor.tier];
|
|
1087
|
+
const metrics2 = getMetrics();
|
|
1088
|
+
const gateway = deps.config.keyPrefix;
|
|
1089
|
+
if (tier.points === Number.MAX_SAFE_INTEGER) {
|
|
1090
|
+
metrics2.rateLimit.inc({
|
|
1091
|
+
gateway,
|
|
1092
|
+
tier: actor.tier,
|
|
1093
|
+
decision: "allowed"
|
|
1094
|
+
});
|
|
1095
|
+
return {
|
|
1096
|
+
allowed: true,
|
|
1097
|
+
remaining: Number.MAX_SAFE_INTEGER,
|
|
1098
|
+
resetAtMs: 0,
|
|
1099
|
+
limit: tier.points
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const key = keyFor(deps.config.keyPrefix, actor.tier, actor.identity);
|
|
1103
|
+
const limit = tier.points + tier.burst;
|
|
1104
|
+
try {
|
|
1105
|
+
const count = await deps.valkey.incrBy(key, 1);
|
|
1106
|
+
if (count === 1) {
|
|
1107
|
+
await deps.valkey.expire(key, tier.duration);
|
|
1108
|
+
}
|
|
1109
|
+
const ttl = await deps.valkey.ttl(key);
|
|
1110
|
+
const resetAtMs = stamp() + Math.max(0, ttl) * 1e3;
|
|
1111
|
+
if (count > limit) {
|
|
1112
|
+
metrics2.rateLimit.inc({
|
|
1113
|
+
gateway,
|
|
1114
|
+
tier: actor.tier,
|
|
1115
|
+
decision: deps.config.observeOnly ? "observed_reject" : "rejected"
|
|
1116
|
+
});
|
|
1117
|
+
return {
|
|
1118
|
+
allowed: false,
|
|
1119
|
+
remaining: 0,
|
|
1120
|
+
resetAtMs,
|
|
1121
|
+
limit
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
metrics2.rateLimit.inc({
|
|
1125
|
+
gateway,
|
|
1126
|
+
tier: actor.tier,
|
|
1127
|
+
decision: "allowed"
|
|
1128
|
+
});
|
|
1129
|
+
return {
|
|
1130
|
+
allowed: true,
|
|
1131
|
+
remaining: Math.max(0, limit - count),
|
|
1132
|
+
resetAtMs,
|
|
1133
|
+
limit
|
|
1134
|
+
};
|
|
1135
|
+
} catch (err) {
|
|
1136
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1137
|
+
logger.warn("[gateway-security] rate-limit valkey error \u2014 fail-open", {
|
|
1138
|
+
key,
|
|
1139
|
+
error: e.message
|
|
1140
|
+
});
|
|
1141
|
+
metrics2.valkeyFailover.inc({
|
|
1142
|
+
gateway
|
|
1143
|
+
});
|
|
1144
|
+
if (deps.config.failClosed) {
|
|
1145
|
+
metrics2.rateLimit.inc({
|
|
1146
|
+
gateway,
|
|
1147
|
+
tier: actor.tier,
|
|
1148
|
+
decision: "rejected"
|
|
1149
|
+
});
|
|
1150
|
+
return {
|
|
1151
|
+
allowed: false,
|
|
1152
|
+
remaining: 0,
|
|
1153
|
+
resetAtMs: stamp() + tier.duration * 1e3,
|
|
1154
|
+
limit
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
metrics2.rateLimit.inc({
|
|
1158
|
+
gateway,
|
|
1159
|
+
tier: actor.tier,
|
|
1160
|
+
decision: "allowed"
|
|
1161
|
+
});
|
|
1162
|
+
return {
|
|
1163
|
+
allowed: true,
|
|
1164
|
+
remaining: -1,
|
|
1165
|
+
resetAtMs: 0,
|
|
1166
|
+
limit
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
}, "checkRateLimit");
|
|
1170
|
+
var createRateLimitPlugin = /* @__PURE__ */ __name((deps) => {
|
|
1171
|
+
return {
|
|
1172
|
+
onRequest: /* @__PURE__ */ __name(async ({ request, fetchAPI, endResponse }) => {
|
|
1173
|
+
const actor = resolveActor((name) => request.headers.get(name));
|
|
1174
|
+
const result = await checkRateLimit(deps, actor);
|
|
1175
|
+
request.__rateLimitResult = result;
|
|
1176
|
+
request.__rateLimitActor = actor;
|
|
1177
|
+
if (!result.allowed) {
|
|
1178
|
+
if (deps.config.observeOnly) {
|
|
1179
|
+
logger.info("[gateway-security] rate-limit would_reject (observe)", {
|
|
1180
|
+
tier: actor.tier,
|
|
1181
|
+
identity: actor.identity,
|
|
1182
|
+
limit: result.limit
|
|
1183
|
+
});
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
const retryAfterS = Math.max(1, Math.ceil((result.resetAtMs - stamp()) / 1e3));
|
|
1187
|
+
const body = JSON.stringify({
|
|
1188
|
+
errors: [
|
|
1189
|
+
{
|
|
1190
|
+
message: "Rate limit exceeded",
|
|
1191
|
+
extensions: {
|
|
1192
|
+
code: "RATE_LIMITED",
|
|
1193
|
+
resetAt: new Date(result.resetAtMs).toISOString()
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
]
|
|
1197
|
+
});
|
|
1198
|
+
const response = new fetchAPI.Response(body, {
|
|
1199
|
+
status: 429,
|
|
1200
|
+
headers: {
|
|
1201
|
+
"content-type": "application/json",
|
|
1202
|
+
"retry-after": String(retryAfterS),
|
|
1203
|
+
"x-ratelimit-limit": String(result.limit),
|
|
1204
|
+
"x-ratelimit-remaining": "0",
|
|
1205
|
+
"x-ratelimit-reset": String(Math.floor(result.resetAtMs / 1e3))
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
endResponse(response);
|
|
1209
|
+
}
|
|
1210
|
+
}, "onRequest"),
|
|
1211
|
+
onResponse: /* @__PURE__ */ __name(({ request, response }) => {
|
|
1212
|
+
const stash = request.__rateLimitResult;
|
|
1213
|
+
if (!stash) return;
|
|
1214
|
+
response.headers.set("x-ratelimit-limit", String(stash.limit));
|
|
1215
|
+
response.headers.set("x-ratelimit-remaining", String(Math.max(0, stash.remaining)));
|
|
1216
|
+
if (stash.resetAtMs > 0) {
|
|
1217
|
+
response.headers.set("x-ratelimit-reset", String(Math.floor(stash.resetAtMs / 1e3)));
|
|
1218
|
+
}
|
|
1219
|
+
}, "onResponse")
|
|
1220
|
+
};
|
|
1221
|
+
}, "createRateLimitPlugin");
|
|
1222
|
+
|
|
1223
|
+
// src/gateway/security/plugins/index.ts
|
|
1224
|
+
var buildSecurityPlugins = /* @__PURE__ */ __name(async (options) => {
|
|
1225
|
+
if (!options.enabled) {
|
|
1226
|
+
logger.warn("[gateway-security] SECURITY_PLUGINS_ENABLED=false \u2014 plugins disabled", {
|
|
1227
|
+
gatewayName: options.gatewayName
|
|
1228
|
+
});
|
|
1229
|
+
return [];
|
|
1230
|
+
}
|
|
1231
|
+
await initializeMetrics();
|
|
1232
|
+
const valkey = await initializeSecurityValkey(options.gatewayName);
|
|
1233
|
+
const plugins = [];
|
|
1234
|
+
const otelPlugin = await createOtelPlugin(options.otel);
|
|
1235
|
+
plugins.push(otelPlugin);
|
|
1236
|
+
const armorPlugins = await createArmorPlugins(options.armor);
|
|
1237
|
+
plugins.push(...armorPlugins);
|
|
1238
|
+
if (options.queryComplexity.enabled) {
|
|
1239
|
+
plugins.push(createQueryComplexityPlugin({
|
|
1240
|
+
config: options.queryComplexity
|
|
1241
|
+
}));
|
|
1242
|
+
}
|
|
1243
|
+
plugins.push(createRateLimitPlugin({
|
|
1244
|
+
valkey,
|
|
1245
|
+
config: options.rateLimit
|
|
1246
|
+
}));
|
|
1247
|
+
plugins.push(createOperationTimeoutPlugin({
|
|
1248
|
+
config: options.operationTimeout
|
|
1249
|
+
}));
|
|
1250
|
+
logger.info("[gateway-security] plugins wired", {
|
|
1251
|
+
gatewayName: options.gatewayName,
|
|
1252
|
+
count: plugins.length,
|
|
1253
|
+
observe: {
|
|
1254
|
+
armor: options.armor.observe,
|
|
1255
|
+
complexity: options.queryComplexity.observe,
|
|
1256
|
+
rateLimit: options.rateLimit.observeOnly
|
|
1257
|
+
},
|
|
1258
|
+
enabled: {
|
|
1259
|
+
complexity: options.queryComplexity.enabled
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
return plugins;
|
|
1263
|
+
}, "buildSecurityPlugins");
|
|
1264
|
+
|
|
1265
|
+
export { DEFAULT_ARMOR_CONFIG, DEFAULT_COST_MAP, DEFAULT_TIERS, MemoryValkey, __resetMetricsForTest, __setSecurityValkeyForTest, buildSecurityPlugins, checkRateLimit, createArmorPlugins, createOperationTimeoutPlugin, createOtelPlugin, createQueryComplexityPlugin, createRateLimitPlugin, getMetrics, getSecurityValkey, initializeMetrics, initializeOtel, initializeSecurityValkey, mergeCostMap, resolveActor, resolveSecurityOptionsFromEnv, resolveTiersFromEnv, scrapeMetrics, security_exports, shutdownOtel };
|
|
1266
|
+
//# sourceMappingURL=chunk-MLW2HPS5.js.map
|
|
1267
|
+
//# sourceMappingURL=chunk-MLW2HPS5.js.map
|