@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.
Files changed (220) hide show
  1. package/README.md +173 -0
  2. package/configs/eslint/index.js +492 -0
  3. package/configs/tsconfig/tsconfig.json +53 -0
  4. package/dist/EventQueueFactory-6NMSGECR.js +8 -0
  5. package/dist/EventQueueFactory-6NMSGECR.js.map +1 -0
  6. package/dist/auth/index.d.ts +66 -0
  7. package/dist/auth/index.js +8 -0
  8. package/dist/auth/index.js.map +1 -0
  9. package/dist/cache/index.d.ts +2 -0
  10. package/dist/cache/index.js +6 -0
  11. package/dist/cache/index.js.map +1 -0
  12. package/dist/captcha/index.d.ts +1 -0
  13. package/dist/captcha/index.js +4 -0
  14. package/dist/captcha/index.js.map +1 -0
  15. package/dist/chunk-2HANKZZM.js +245 -0
  16. package/dist/chunk-2HANKZZM.js.map +1 -0
  17. package/dist/chunk-2SD5L2F2.js +816 -0
  18. package/dist/chunk-2SD5L2F2.js.map +1 -0
  19. package/dist/chunk-3O2RQOPV.js +1068 -0
  20. package/dist/chunk-3O2RQOPV.js.map +1 -0
  21. package/dist/chunk-4B43DZIS.js +560 -0
  22. package/dist/chunk-4B43DZIS.js.map +1 -0
  23. package/dist/chunk-4XAEES25.js +2444 -0
  24. package/dist/chunk-4XAEES25.js.map +1 -0
  25. package/dist/chunk-4XGGWW36.js +1405 -0
  26. package/dist/chunk-4XGGWW36.js.map +1 -0
  27. package/dist/chunk-5LXDPGDH.js +245 -0
  28. package/dist/chunk-5LXDPGDH.js.map +1 -0
  29. package/dist/chunk-AI7L22ZJ.js +472 -0
  30. package/dist/chunk-AI7L22ZJ.js.map +1 -0
  31. package/dist/chunk-CBZNP6RF.js +286 -0
  32. package/dist/chunk-CBZNP6RF.js.map +1 -0
  33. package/dist/chunk-ELLX3K54.js +613 -0
  34. package/dist/chunk-ELLX3K54.js.map +1 -0
  35. package/dist/chunk-F5XEAGHQ.js +1875 -0
  36. package/dist/chunk-F5XEAGHQ.js.map +1 -0
  37. package/dist/chunk-FNZEQOJY.js +628 -0
  38. package/dist/chunk-FNZEQOJY.js.map +1 -0
  39. package/dist/chunk-GTEMQXCA.js +516 -0
  40. package/dist/chunk-GTEMQXCA.js.map +1 -0
  41. package/dist/chunk-HIWTWIGQ.js +226 -0
  42. package/dist/chunk-HIWTWIGQ.js.map +1 -0
  43. package/dist/chunk-IYE2NTRX.js +764 -0
  44. package/dist/chunk-IYE2NTRX.js.map +1 -0
  45. package/dist/chunk-J26YGR2U.js +1484 -0
  46. package/dist/chunk-J26YGR2U.js.map +1 -0
  47. package/dist/chunk-JG2FTZPI.js +509 -0
  48. package/dist/chunk-JG2FTZPI.js.map +1 -0
  49. package/dist/chunk-JVBTNSPG.js +1551 -0
  50. package/dist/chunk-JVBTNSPG.js.map +1 -0
  51. package/dist/chunk-L7ZMN2FJ.js +12 -0
  52. package/dist/chunk-L7ZMN2FJ.js.map +1 -0
  53. package/dist/chunk-M5IMU3US.js +473 -0
  54. package/dist/chunk-M5IMU3US.js.map +1 -0
  55. package/dist/chunk-MLW2HPS5.js +1267 -0
  56. package/dist/chunk-MLW2HPS5.js.map +1 -0
  57. package/dist/chunk-MNCGUYLJ.js +269 -0
  58. package/dist/chunk-MNCGUYLJ.js.map +1 -0
  59. package/dist/chunk-MOCUBSTW.js +373 -0
  60. package/dist/chunk-MOCUBSTW.js.map +1 -0
  61. package/dist/chunk-NGMEI42S.js +103 -0
  62. package/dist/chunk-NGMEI42S.js.map +1 -0
  63. package/dist/chunk-NUB5GQTU.js +270 -0
  64. package/dist/chunk-NUB5GQTU.js.map +1 -0
  65. package/dist/chunk-OVSYMNCI.js +145 -0
  66. package/dist/chunk-OVSYMNCI.js.map +1 -0
  67. package/dist/chunk-QLVKMWFA.js +94 -0
  68. package/dist/chunk-QLVKMWFA.js.map +1 -0
  69. package/dist/chunk-SAG6ZYWQ.js +89 -0
  70. package/dist/chunk-SAG6ZYWQ.js.map +1 -0
  71. package/dist/chunk-SAK2JCA7.js +198 -0
  72. package/dist/chunk-SAK2JCA7.js.map +1 -0
  73. package/dist/chunk-SBS4S2PJ.js +100 -0
  74. package/dist/chunk-SBS4S2PJ.js.map +1 -0
  75. package/dist/chunk-SNEOLWXT.js +148 -0
  76. package/dist/chunk-SNEOLWXT.js.map +1 -0
  77. package/dist/chunk-TKBD2ONF.js +564 -0
  78. package/dist/chunk-TKBD2ONF.js.map +1 -0
  79. package/dist/chunk-UBXFIIAW.js +127 -0
  80. package/dist/chunk-UBXFIIAW.js.map +1 -0
  81. package/dist/chunk-VI4VSNTI.js +57 -0
  82. package/dist/chunk-VI4VSNTI.js.map +1 -0
  83. package/dist/chunk-VWCN5PMD.js +268 -0
  84. package/dist/chunk-VWCN5PMD.js.map +1 -0
  85. package/dist/chunk-XC2YD6CF.js +913 -0
  86. package/dist/chunk-XC2YD6CF.js.map +1 -0
  87. package/dist/chunk-XE4OXN2W.js +10 -0
  88. package/dist/chunk-XE4OXN2W.js.map +1 -0
  89. package/dist/chunk-YMZEUTD2.js +110 -0
  90. package/dist/chunk-YMZEUTD2.js.map +1 -0
  91. package/dist/chunk-Z4IRLBWO.js +758 -0
  92. package/dist/chunk-Z4IRLBWO.js.map +1 -0
  93. package/dist/client/internal-client.d.ts +7 -0
  94. package/dist/client/internal-client.js +5 -0
  95. package/dist/client/internal-client.js.map +1 -0
  96. package/dist/client-CSjIrYKS.d.ts +77 -0
  97. package/dist/config-UKE2E3BW.js +4 -0
  98. package/dist/config-UKE2E3BW.js.map +1 -0
  99. package/dist/crypto/index.d.ts +1 -0
  100. package/dist/crypto/index.js +4 -0
  101. package/dist/crypto/index.js.map +1 -0
  102. package/dist/database/index.d.ts +117 -0
  103. package/dist/database/index.js +7 -0
  104. package/dist/database/index.js.map +1 -0
  105. package/dist/dataloader/index.d.ts +1 -0
  106. package/dist/dataloader/index.js +4 -0
  107. package/dist/dataloader/index.js.map +1 -0
  108. package/dist/esm-K6H7EIWK.js +1450 -0
  109. package/dist/esm-K6H7EIWK.js.map +1 -0
  110. package/dist/event-queue/index.d.ts +6 -0
  111. package/dist/event-queue/index.js +10 -0
  112. package/dist/event-queue/index.js.map +1 -0
  113. package/dist/export/index.d.ts +2 -0
  114. package/dist/export/index.js +9 -0
  115. package/dist/export/index.js.map +1 -0
  116. package/dist/feature-flags/index.d.ts +1 -0
  117. package/dist/feature-flags/index.js +4 -0
  118. package/dist/feature-flags/index.js.map +1 -0
  119. package/dist/gateway/index.d.ts +4 -0
  120. package/dist/gateway/index.js +8 -0
  121. package/dist/gateway/index.js.map +1 -0
  122. package/dist/gateway/security/index.d.ts +2 -0
  123. package/dist/gateway/security/index.js +5 -0
  124. package/dist/gateway/security/index.js.map +1 -0
  125. package/dist/graph/index.d.ts +9 -0
  126. package/dist/graph/index.js +13 -0
  127. package/dist/graph/index.js.map +1 -0
  128. package/dist/graph-emitter-Bk0FqD53.d.ts +47 -0
  129. package/dist/graphql/index.d.ts +2 -0
  130. package/dist/graphql/index.js +7 -0
  131. package/dist/graphql/index.js.map +1 -0
  132. package/dist/graphql/services/index.d.ts +24 -0
  133. package/dist/graphql/services/index.js +5 -0
  134. package/dist/graphql/services/index.js.map +1 -0
  135. package/dist/graphql-BAVrUgBF.d.ts +22 -0
  136. package/dist/i18n/index.d.ts +2 -0
  137. package/dist/i18n/index.js +5 -0
  138. package/dist/i18n/index.js.map +1 -0
  139. package/dist/i18n-DA6IPnYy.d.ts +101 -0
  140. package/dist/index-4gKQKZ2B.d.ts +71 -0
  141. package/dist/index-B18rrFru.d.ts +42 -0
  142. package/dist/index-B70b5mYS.d.ts +33 -0
  143. package/dist/index-BMq3lFjj.d.ts +247 -0
  144. package/dist/index-BODgvRtL.d.ts +316 -0
  145. package/dist/index-BS2idbnr.d.ts +874 -0
  146. package/dist/index-BV3G48cH.d.ts +124 -0
  147. package/dist/index-CHbVw5M7.d.ts +73 -0
  148. package/dist/index-Cq_xNjcv.d.ts +602 -0
  149. package/dist/index-CrQgidpx.d.ts +77 -0
  150. package/dist/index-CyVEg86m.d.ts +309 -0
  151. package/dist/index-DOcBnmMe.d.ts +166 -0
  152. package/dist/index-DSOpj8DI.d.ts +17 -0
  153. package/dist/index-DW9q22GH.d.ts +75 -0
  154. package/dist/index-DlFDFyWD.d.ts +55 -0
  155. package/dist/index-DoK0bjhH.d.ts +607 -0
  156. package/dist/index-Dtf6FBaD.d.ts +199 -0
  157. package/dist/index-KVWGHMbS.d.ts +415 -0
  158. package/dist/index-PaojfSdd.d.ts +71 -0
  159. package/dist/index-RU_M5c74.d.ts +163 -0
  160. package/dist/index-UhkH2eaB.d.ts +192 -0
  161. package/dist/index-e_18DzAg.d.ts +18 -0
  162. package/dist/index-jOuh_TKn.d.ts +59 -0
  163. package/dist/index.d.ts +584 -0
  164. package/dist/index.js +2626 -0
  165. package/dist/index.js.map +1 -0
  166. package/dist/init/index.d.ts +2 -0
  167. package/dist/init/index.js +12 -0
  168. package/dist/init/index.js.map +1 -0
  169. package/dist/instrumentation/index.d.ts +3 -0
  170. package/dist/instrumentation/index.js +5 -0
  171. package/dist/instrumentation/index.js.map +1 -0
  172. package/dist/layer/index.d.ts +2 -0
  173. package/dist/layer/index.js +6 -0
  174. package/dist/layer/index.js.map +1 -0
  175. package/dist/layer-DDip18KY.d.ts +67 -0
  176. package/dist/logger/index.d.ts +2 -0
  177. package/dist/logger/index.js +4 -0
  178. package/dist/logger/index.js.map +1 -0
  179. package/dist/middleware/index.d.ts +3 -0
  180. package/dist/middleware/index.js +5 -0
  181. package/dist/middleware/index.js.map +1 -0
  182. package/dist/notifications/index.d.ts +4 -0
  183. package/dist/notifications/index.js +7 -0
  184. package/dist/notifications/index.js.map +1 -0
  185. package/dist/observability/index.d.ts +3 -0
  186. package/dist/observability/index.js +5 -0
  187. package/dist/observability/index.js.map +1 -0
  188. package/dist/pino-logger-RYLGXPHZ.js +5 -0
  189. package/dist/pino-logger-RYLGXPHZ.js.map +1 -0
  190. package/dist/prisma/generator/index.d.ts +1 -0
  191. package/dist/prisma/generator/index.js +228 -0
  192. package/dist/prisma/generator/index.js.map +1 -0
  193. package/dist/prisma/index.d.ts +5 -0
  194. package/dist/prisma/index.js +12 -0
  195. package/dist/prisma/index.js.map +1 -0
  196. package/dist/prisma-extension-krXnYnrN.d.ts +30 -0
  197. package/dist/rbac/index.d.ts +116 -0
  198. package/dist/rbac/index.js +7 -0
  199. package/dist/rbac/index.js.map +1 -0
  200. package/dist/sdk-THO3IWEE.js +136 -0
  201. package/dist/sdk-THO3IWEE.js.map +1 -0
  202. package/dist/server/index.d.ts +359 -0
  203. package/dist/server/index.js +884 -0
  204. package/dist/server/index.js.map +1 -0
  205. package/dist/telemetry/index.d.ts +83 -0
  206. package/dist/telemetry/index.js +209 -0
  207. package/dist/telemetry/index.js.map +1 -0
  208. package/dist/temporal/index.d.ts +148 -0
  209. package/dist/temporal/index.js +6 -0
  210. package/dist/temporal/index.js.map +1 -0
  211. package/dist/tracker/index.d.ts +5 -0
  212. package/dist/tracker/index.js +9 -0
  213. package/dist/tracker/index.js.map +1 -0
  214. package/dist/types-Bo2wkjSP.d.ts +35 -0
  215. package/dist/utils/index.d.ts +1 -0
  216. package/dist/utils/index.js +4 -0
  217. package/dist/utils/index.js.map +1 -0
  218. package/dist/workspace-guard-LHBNK4TG.js +5 -0
  219. package/dist/workspace-guard-LHBNK4TG.js.map +1 -0
  220. 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