@deenruv/harden-plugin 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ # License 1
2
+
3
+ The MIT License
4
+
5
+ Copyright (c) 2025-present Aexol
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12
+
13
+ # License 2
14
+
15
+ The MIT License
16
+
17
+ Copyright (c) 2018-2025 Michael Bromley
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @deenruv/harden-plugin
2
+
3
+ Hardens your Deenruv GraphQL APIs against abuse and attacks. Recommended for all production deployments.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @deenruv/harden-plugin
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ ```typescript
14
+ import { HardenPlugin } from '@deenruv/harden-plugin';
15
+
16
+ const config = {
17
+ plugins: [
18
+ HardenPlugin.init({
19
+ maxQueryComplexity: 650,
20
+ apiMode: process.env.APP_ENV === 'dev' ? 'dev' : 'prod',
21
+ }),
22
+ ],
23
+ };
24
+ ```
25
+
26
+ **Options:**
27
+
28
+ | Option | Type | Default | Description |
29
+ |--------|------|---------|-------------|
30
+ | `maxQueryComplexity` | `number` | `1000` | Maximum permitted query complexity score |
31
+ | `apiMode` | `'dev' \| 'prod'` | `'prod'` | In `prod` mode, disables introspection and GraphQL playground |
32
+ | `hideFieldSuggestions` | `boolean` | `true` | Prevents field name suggestions in error messages (blocks schema sniffing) |
33
+ | `logComplexityScore` | `boolean` | `false` | Logs complexity score breakdown for each query (useful for tuning) |
34
+ | `customComplexityFactors` | `{ [path: string]: number }` | - | Custom complexity weights for specific fields |
35
+ | `queryComplexityEstimators` | `ComplexityEstimator[]` | Deenruv default | Custom estimator functions for complexity calculation |
36
+
37
+ ## Features
38
+
39
+ - **Query complexity analysis** - Rejects overly complex queries that could overload server resources (powered by [graphql-query-complexity](https://www.npmjs.com/package/graphql-query-complexity))
40
+ - **Introspection control** - Disables introspection and GraphQL playground in production mode
41
+ - **Field suggestion hiding** - Removes field name suggestions from validation errors, preventing trial-and-error schema discovery
42
+ - **Complexity logging** - Optional detailed logging of query complexity scores for tuning the `maxQueryComplexity` threshold
43
+ - **Custom complexity weights** - Per-field complexity factors for expensive custom operations
44
+ - **Deenruv-optimized estimator** - Default complexity estimator tuned specifically for the Deenruv API (applies a factor of 1000 for list queries without a `take` argument)
45
+
46
+ ## Admin UI
47
+
48
+ Server-only plugin. No Admin UI extensions.
49
+
50
+ ## API Extensions
51
+
52
+ No GraphQL API extensions. This plugin configures Apollo Server plugins for query analysis and validation.
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./src/harden.plugin";
2
+ export * from "./src/types";
3
+ export * from "./src/middleware/query-complexity-plugin";
package/lib/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./src/harden.plugin"), exports);
18
+ __exportStar(require("./src/types"), exports);
19
+ __exportStar(require("./src/middleware/query-complexity-plugin"), exports);
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAoC;AACpC,8CAA4B;AAC5B,2EAAyD"}
@@ -0,0 +1,2 @@
1
+ export declare const loggerCtx = "HardenPlugin";
2
+ export declare const HARDEN_PLUGIN_OPTIONS: unique symbol;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HARDEN_PLUGIN_OPTIONS = exports.loggerCtx = void 0;
4
+ exports.loggerCtx = "HardenPlugin";
5
+ exports.HARDEN_PLUGIN_OPTIONS = Symbol("HARDEN_PLUGIN_OPTIONS");
6
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,SAAS,GAAG,cAAc,CAAC;AAC3B,QAAA,qBAAqB,GAAG,MAAM,CAAC,uBAAuB,CAAC,CAAC"}
@@ -0,0 +1,143 @@
1
+ import { HardenPluginOptions } from "./types";
2
+ /**
3
+ * @description
4
+ * The HardenPlugin hardens the Shop and Admin GraphQL APIs against attacks and abuse.
5
+ *
6
+ * - It analyzes the complexity on incoming graphql queries and rejects queries that are too complex and
7
+ * could be used to overload the resources of the server.
8
+ * - It disables dev-mode API features such as introspection and the GraphQL playground app.
9
+ * - It removes field name suggestions to prevent trial-and-error schema sniffing.
10
+ *
11
+ * It is a recommended plugin for all production configurations.
12
+ *
13
+ * ## Installation
14
+ *
15
+ * `yarn add \@deenruv/harden-plugin`
16
+ *
17
+ * or
18
+ *
19
+ * `npm install \@deenruv/harden-plugin`
20
+ *
21
+ * Then add the `HardenPlugin`, calling the `.init()` method with {@link HardenPluginOptions}:
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { HardenPlugin } from '\@deenruv/harden-plugin';
26
+ *
27
+ * const config: DeenruvConfig = {
28
+ * // Add an instance of the plugin to the plugins array
29
+ * plugins: [
30
+ * HardenPlugin.init({
31
+ * maxQueryComplexity: 650,
32
+ * apiMode: process.env.APP_ENV === 'dev' ? 'dev' : 'prod',
33
+ * }),
34
+ * ],
35
+ * };
36
+ * ```
37
+ *
38
+ * ## Setting the max query complexity
39
+ *
40
+ * The `maxQueryComplexity` option determines how complex a query can be. The complexity of a query relates to how many, and how
41
+ * deeply-nested are the fields being selected, and is intended to roughly correspond to the amount of server resources that would
42
+ * be required to resolve that query.
43
+ *
44
+ * The goal of this setting is to prevent attacks in which a malicious actor crafts a very complex query in order to overwhelm your
45
+ * server resources. Here's an example of a request which would likely overwhelm a Deenruv server:
46
+ *
47
+ * ```GraphQL
48
+ * query EvilQuery {
49
+ * products {
50
+ * items {
51
+ * collections {
52
+ * productVariants {
53
+ * items {
54
+ * product {
55
+ * collections {
56
+ * productVariants {
57
+ * items {
58
+ * product {
59
+ * variants {
60
+ * name
61
+ * }
62
+ * }
63
+ * }
64
+ * }
65
+ * }
66
+ * }
67
+ * }
68
+ * }
69
+ * }
70
+ * }
71
+ * }
72
+ * }
73
+ * ```
74
+ *
75
+ * This evil query has a complexity score of 2,443,203 - much greater than the default of 1,000!
76
+ *
77
+ * The complexity score is calculated by the [graphql-query-complexity library](https://www.npmjs.com/package/graphql-query-complexity),
78
+ * and by default uses the {@link defaultDeenruvComplexityEstimator}, which is tuned specifically to the Deenruv Shop API.
79
+ *
80
+ * :::caution
81
+ * Note: By default, if the "take" argument is omitted from a list query (e.g. the `products` or `collections` query), a default factor of 1000 is applied.
82
+ * :::
83
+ *
84
+ * The optimal max complexity score will vary depending on:
85
+ *
86
+ * - The requirements of your storefront and other clients using the Shop API
87
+ * - The resources available to your server
88
+ *
89
+ * You should aim to set the maximum as low as possible while still being able to service all the requests required.
90
+ * This will take some manual tuning.
91
+ * While tuning the max, you can turn on the `logComplexityScore` to get a detailed breakdown of the complexity of each query, as well as how
92
+ * that total score is derived from its child fields:
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { HardenPlugin } from '\@deenruv/harden-plugin';
97
+ *
98
+ * const config: DeenruvConfig = {
99
+ * // A detailed summary is logged at the "debug" level
100
+ * logger: new DefaultLogger({ level: LogLevel.Debug }),
101
+ * plugins: [
102
+ * HardenPlugin.init({
103
+ * maxQueryComplexity: 650,
104
+ * logComplexityScore: true,
105
+ * }),
106
+ * ],
107
+ * };
108
+ * ```
109
+ *
110
+ * With logging configured as above, the following query:
111
+ *
112
+ * ```GraphQL
113
+ * query ProductList {
114
+ * products(options: { take: 5 }) {
115
+ * items {
116
+ * id
117
+ * name
118
+ * featuredAsset {
119
+ * preview
120
+ * }
121
+ * }
122
+ * }
123
+ * }
124
+ * ```
125
+ * will log the following breakdown:
126
+ *
127
+ * ```sh
128
+ * debug 16/12/22, 14:12 - [HardenPlugin] Calculating complexity of [ProductList]
129
+ * debug 16/12/22, 14:12 - [HardenPlugin] Product.id: ID! childComplexity: 0, score: 1
130
+ * debug 16/12/22, 14:12 - [HardenPlugin] Product.name: String! childComplexity: 0, score: 1
131
+ * debug 16/12/22, 14:12 - [HardenPlugin] Asset.preview: String! childComplexity: 0, score: 1
132
+ * debug 16/12/22, 14:12 - [HardenPlugin] Product.featuredAsset: Asset childComplexity: 1, score: 2
133
+ * debug 16/12/22, 14:12 - [HardenPlugin] ProductList.items: [Product!]! childComplexity: 4, score: 20
134
+ * debug 16/12/22, 14:12 - [HardenPlugin] Query.products: ProductList! childComplexity: 20, score: 35
135
+ * verbose 16/12/22, 14:12 - [HardenPlugin] Query complexity [ProductList]: 35
136
+ * ```
137
+ *
138
+ * @docsCategory core plugins/HardenPlugin
139
+ */
140
+ export declare class HardenPlugin {
141
+ static options: HardenPluginOptions;
142
+ static init(options: HardenPluginOptions): typeof HardenPlugin;
143
+ }
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var HardenPlugin_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.HardenPlugin = void 0;
11
+ const core_1 = require("@deenruv/core");
12
+ const constants_1 = require("./constants");
13
+ const hide_validation_errors_plugin_1 = require("./middleware/hide-validation-errors-plugin");
14
+ const query_complexity_plugin_1 = require("./middleware/query-complexity-plugin");
15
+ /**
16
+ * @description
17
+ * The HardenPlugin hardens the Shop and Admin GraphQL APIs against attacks and abuse.
18
+ *
19
+ * - It analyzes the complexity on incoming graphql queries and rejects queries that are too complex and
20
+ * could be used to overload the resources of the server.
21
+ * - It disables dev-mode API features such as introspection and the GraphQL playground app.
22
+ * - It removes field name suggestions to prevent trial-and-error schema sniffing.
23
+ *
24
+ * It is a recommended plugin for all production configurations.
25
+ *
26
+ * ## Installation
27
+ *
28
+ * `yarn add \@deenruv/harden-plugin`
29
+ *
30
+ * or
31
+ *
32
+ * `npm install \@deenruv/harden-plugin`
33
+ *
34
+ * Then add the `HardenPlugin`, calling the `.init()` method with {@link HardenPluginOptions}:
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { HardenPlugin } from '\@deenruv/harden-plugin';
39
+ *
40
+ * const config: DeenruvConfig = {
41
+ * // Add an instance of the plugin to the plugins array
42
+ * plugins: [
43
+ * HardenPlugin.init({
44
+ * maxQueryComplexity: 650,
45
+ * apiMode: process.env.APP_ENV === 'dev' ? 'dev' : 'prod',
46
+ * }),
47
+ * ],
48
+ * };
49
+ * ```
50
+ *
51
+ * ## Setting the max query complexity
52
+ *
53
+ * The `maxQueryComplexity` option determines how complex a query can be. The complexity of a query relates to how many, and how
54
+ * deeply-nested are the fields being selected, and is intended to roughly correspond to the amount of server resources that would
55
+ * be required to resolve that query.
56
+ *
57
+ * The goal of this setting is to prevent attacks in which a malicious actor crafts a very complex query in order to overwhelm your
58
+ * server resources. Here's an example of a request which would likely overwhelm a Deenruv server:
59
+ *
60
+ * ```GraphQL
61
+ * query EvilQuery {
62
+ * products {
63
+ * items {
64
+ * collections {
65
+ * productVariants {
66
+ * items {
67
+ * product {
68
+ * collections {
69
+ * productVariants {
70
+ * items {
71
+ * product {
72
+ * variants {
73
+ * name
74
+ * }
75
+ * }
76
+ * }
77
+ * }
78
+ * }
79
+ * }
80
+ * }
81
+ * }
82
+ * }
83
+ * }
84
+ * }
85
+ * }
86
+ * ```
87
+ *
88
+ * This evil query has a complexity score of 2,443,203 - much greater than the default of 1,000!
89
+ *
90
+ * The complexity score is calculated by the [graphql-query-complexity library](https://www.npmjs.com/package/graphql-query-complexity),
91
+ * and by default uses the {@link defaultDeenruvComplexityEstimator}, which is tuned specifically to the Deenruv Shop API.
92
+ *
93
+ * :::caution
94
+ * Note: By default, if the "take" argument is omitted from a list query (e.g. the `products` or `collections` query), a default factor of 1000 is applied.
95
+ * :::
96
+ *
97
+ * The optimal max complexity score will vary depending on:
98
+ *
99
+ * - The requirements of your storefront and other clients using the Shop API
100
+ * - The resources available to your server
101
+ *
102
+ * You should aim to set the maximum as low as possible while still being able to service all the requests required.
103
+ * This will take some manual tuning.
104
+ * While tuning the max, you can turn on the `logComplexityScore` to get a detailed breakdown of the complexity of each query, as well as how
105
+ * that total score is derived from its child fields:
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { HardenPlugin } from '\@deenruv/harden-plugin';
110
+ *
111
+ * const config: DeenruvConfig = {
112
+ * // A detailed summary is logged at the "debug" level
113
+ * logger: new DefaultLogger({ level: LogLevel.Debug }),
114
+ * plugins: [
115
+ * HardenPlugin.init({
116
+ * maxQueryComplexity: 650,
117
+ * logComplexityScore: true,
118
+ * }),
119
+ * ],
120
+ * };
121
+ * ```
122
+ *
123
+ * With logging configured as above, the following query:
124
+ *
125
+ * ```GraphQL
126
+ * query ProductList {
127
+ * products(options: { take: 5 }) {
128
+ * items {
129
+ * id
130
+ * name
131
+ * featuredAsset {
132
+ * preview
133
+ * }
134
+ * }
135
+ * }
136
+ * }
137
+ * ```
138
+ * will log the following breakdown:
139
+ *
140
+ * ```sh
141
+ * debug 16/12/22, 14:12 - [HardenPlugin] Calculating complexity of [ProductList]
142
+ * debug 16/12/22, 14:12 - [HardenPlugin] Product.id: ID! childComplexity: 0, score: 1
143
+ * debug 16/12/22, 14:12 - [HardenPlugin] Product.name: String! childComplexity: 0, score: 1
144
+ * debug 16/12/22, 14:12 - [HardenPlugin] Asset.preview: String! childComplexity: 0, score: 1
145
+ * debug 16/12/22, 14:12 - [HardenPlugin] Product.featuredAsset: Asset childComplexity: 1, score: 2
146
+ * debug 16/12/22, 14:12 - [HardenPlugin] ProductList.items: [Product!]! childComplexity: 4, score: 20
147
+ * debug 16/12/22, 14:12 - [HardenPlugin] Query.products: ProductList! childComplexity: 20, score: 35
148
+ * verbose 16/12/22, 14:12 - [HardenPlugin] Query complexity [ProductList]: 35
149
+ * ```
150
+ *
151
+ * @docsCategory core plugins/HardenPlugin
152
+ */
153
+ let HardenPlugin = HardenPlugin_1 = class HardenPlugin {
154
+ static init(options) {
155
+ this.options = options;
156
+ return HardenPlugin_1;
157
+ }
158
+ };
159
+ exports.HardenPlugin = HardenPlugin;
160
+ exports.HardenPlugin = HardenPlugin = HardenPlugin_1 = __decorate([
161
+ (0, core_1.DeenruvPlugin)({
162
+ providers: [
163
+ {
164
+ provide: constants_1.HARDEN_PLUGIN_OPTIONS,
165
+ useFactory: () => HardenPlugin.options,
166
+ },
167
+ ],
168
+ configuration: (config) => {
169
+ if (HardenPlugin.options.hideFieldSuggestions !== false) {
170
+ core_1.Logger.verbose("Configuring HideValidationErrorsPlugin", constants_1.loggerCtx);
171
+ config.apiOptions.apolloServerPlugins.push(new hide_validation_errors_plugin_1.HideValidationErrorsPlugin());
172
+ }
173
+ config.apiOptions.apolloServerPlugins.push(new query_complexity_plugin_1.QueryComplexityPlugin(HardenPlugin.options));
174
+ if (HardenPlugin.options.apiMode !== "dev") {
175
+ config.apiOptions.adminApiDebug = false;
176
+ config.apiOptions.shopApiDebug = false;
177
+ config.apiOptions.introspection = false;
178
+ }
179
+ return config;
180
+ },
181
+ compatibility: "^0.0.0",
182
+ })
183
+ ], HardenPlugin);
184
+ //# sourceMappingURL=harden.plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harden.plugin.js","sourceRoot":"","sources":["../../src/harden.plugin.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,wCAAsD;AAEtD,2CAA+D;AAC/D,8FAAwF;AACxF,kFAA6E;AAG7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyIG;AA4BI,IAAM,YAAY,oBAAlB,MAAM,YAAY;IAGvB,MAAM,CAAC,IAAI,CAAC,OAA4B;QACtC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,OAAO,cAAY,CAAC;IACtB,CAAC;CACF,CAAA;AAPY,oCAAY;uBAAZ,YAAY;IA3BxB,IAAA,oBAAa,EAAC;QACb,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,iCAAqB;gBAC9B,UAAU,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO;aACvC;SACF;QACD,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE;YACxB,IAAI,YAAY,CAAC,OAAO,CAAC,oBAAoB,KAAK,KAAK,EAAE,CAAC;gBACxD,aAAM,CAAC,OAAO,CAAC,wCAAwC,EAAE,qBAAS,CAAC,CAAC;gBACpE,MAAM,CAAC,UAAU,CAAC,mBAAmB,CAAC,IAAI,CACxC,IAAI,0DAA0B,EAAE,CACjC,CAAC;YACJ,CAAC;YACD,MAAM,CAAC,UAAU,CAAC,mBAAmB,CAAC,IAAI,CACxC,IAAI,+CAAqB,CAAC,YAAY,CAAC,OAAO,CAAC,CAChD,CAAC;YACF,IAAI,YAAY,CAAC,OAAO,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;gBAC3C,MAAM,CAAC,UAAU,CAAC,aAAa,GAAG,KAAK,CAAC;gBACxC,MAAM,CAAC,UAAU,CAAC,YAAY,GAAG,KAAK,CAAC;gBACvC,MAAM,CAAC,UAAU,CAAC,aAAa,GAAG,KAAK,CAAC;YAC1C,CAAC;YAED,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,aAAa,EAAE,QAAQ;KACxB,CAAC;GACW,YAAY,CAOxB"}
@@ -0,0 +1,9 @@
1
+ import { ApolloServerPlugin, GraphQLRequestListener } from "@apollo/server";
2
+ /**
3
+ * @description
4
+ * Hides graphql-js suggestions when invalid field names are given.
5
+ * Based on ideas discussed in https://github.com/apollographql/apollo-server/issues/3919
6
+ */
7
+ export declare class HideValidationErrorsPlugin implements ApolloServerPlugin {
8
+ requestDidStart(): Promise<GraphQLRequestListener<any>>;
9
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HideValidationErrorsPlugin = void 0;
4
+ const index_1 = require("graphql/error/index");
5
+ /**
6
+ * @description
7
+ * Hides graphql-js suggestions when invalid field names are given.
8
+ * Based on ideas discussed in https://github.com/apollographql/apollo-server/issues/3919
9
+ */
10
+ class HideValidationErrorsPlugin {
11
+ async requestDidStart() {
12
+ return {
13
+ willSendResponse: async (requestContext) => {
14
+ const { errors } = requestContext;
15
+ if (errors) {
16
+ requestContext.response.errors = errors.map((err) => {
17
+ if (err.message.includes("Did you mean")) {
18
+ return new index_1.GraphQLError("Invalid request");
19
+ }
20
+ else {
21
+ return err;
22
+ }
23
+ });
24
+ }
25
+ },
26
+ };
27
+ }
28
+ }
29
+ exports.HideValidationErrorsPlugin = HideValidationErrorsPlugin;
30
+ //# sourceMappingURL=hide-validation-errors-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hide-validation-errors-plugin.js","sourceRoot":"","sources":["../../../src/middleware/hide-validation-errors-plugin.ts"],"names":[],"mappings":";;;AACA,+CAAmD;AAEnD;;;;GAIG;AACH,MAAa,0BAA0B;IACrC,KAAK,CAAC,eAAe;QACnB,OAAO;YACL,gBAAgB,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;gBACzC,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC;gBAClC,IAAI,MAAM,EAAE,CAAC;oBACV,cAAc,CAAC,QAAgB,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;wBAC3D,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;4BACzC,OAAO,IAAI,oBAAY,CAAC,iBAAiB,CAAC,CAAC;wBAC7C,CAAC;6BAAM,CAAC;4BACN,OAAO,GAAG,CAAC;wBACb,CAAC;oBACH,CAAC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF;AAjBD,gEAiBC"}
@@ -0,0 +1,25 @@
1
+ import { ApolloServerPlugin, GraphQLRequestListener, GraphQLRequestContext } from "@apollo/server";
2
+ import { ComplexityEstimatorArgs } from "graphql-query-complexity";
3
+ import { HardenPluginOptions } from "../types";
4
+ /**
5
+ * @description
6
+ * Implements query complexity analysis on Shop API requests.
7
+ */
8
+ export declare class QueryComplexityPlugin implements ApolloServerPlugin {
9
+ private options;
10
+ constructor(options: HardenPluginOptions);
11
+ requestDidStart({ schema, }: GraphQLRequestContext<any>): Promise<GraphQLRequestListener<any>>;
12
+ }
13
+ /**
14
+ * @description
15
+ * A complexity estimator which takes into account List and PaginatedList types and can
16
+ * be further configured by providing a customComplexityFactors object.
17
+ *
18
+ * When selecting PaginatedList types, the "take" argument is used to estimate a complexity
19
+ * factor. If the "take" argument is omitted, a default factor of 1000 is applied.
20
+ *
21
+ * @docsCategory core plugins/HardenPlugin
22
+ */
23
+ export declare function defaultDeenruvComplexityEstimator(customComplexityFactors: {
24
+ [path: string]: number;
25
+ }, logFieldScores: boolean): (options: ComplexityEstimatorArgs) => number | void;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultDeenruvComplexityEstimator = exports.QueryComplexityPlugin = void 0;
4
+ const core_1 = require("@deenruv/core");
5
+ const graphql_1 = require("graphql");
6
+ const graphql_query_complexity_1 = require("graphql-query-complexity");
7
+ const constants_1 = require("../constants");
8
+ /**
9
+ * @description
10
+ * Implements query complexity analysis on Shop API requests.
11
+ */
12
+ class QueryComplexityPlugin {
13
+ constructor(options) {
14
+ this.options = options;
15
+ }
16
+ async requestDidStart({ schema, }) {
17
+ var _a;
18
+ const maxQueryComplexity = (_a = this.options.maxQueryComplexity) !== null && _a !== void 0 ? _a : 1000;
19
+ return {
20
+ didResolveOperation: async ({ request, document }) => {
21
+ var _a, _b, _c, _d, _e, _f;
22
+ if (isAdminApi(schema)) {
23
+ // We don't want to apply the cost analysis on the
24
+ // Admin API, since any expensive operations would require
25
+ // an authenticated session.
26
+ return;
27
+ }
28
+ const query = request.operationName
29
+ ? (0, graphql_1.separateOperations)(document)[request.operationName]
30
+ : document;
31
+ if (this.options.logComplexityScore === true) {
32
+ core_1.Logger.debug(`Calculating complexity of "${(_a = request.operationName) !== null && _a !== void 0 ? _a : "anonymous"}"`, constants_1.loggerCtx);
33
+ }
34
+ const complexity = (0, graphql_query_complexity_1.getComplexity)({
35
+ schema,
36
+ query,
37
+ variables: request.variables,
38
+ estimators: (_b = this.options.queryComplexityEstimators) !== null && _b !== void 0 ? _b : [
39
+ defaultDeenruvComplexityEstimator((_c = this.options.customComplexityFactors) !== null && _c !== void 0 ? _c : {}, (_d = this.options.logComplexityScore) !== null && _d !== void 0 ? _d : false),
40
+ (0, graphql_query_complexity_1.simpleEstimator)({ defaultComplexity: 1 }),
41
+ ],
42
+ });
43
+ if (this.options.logComplexityScore === true) {
44
+ core_1.Logger.verbose(`Query complexity "${(_e = request.operationName) !== null && _e !== void 0 ? _e : "anonymous"}": ${complexity}`, constants_1.loggerCtx);
45
+ }
46
+ if (complexity >= maxQueryComplexity) {
47
+ core_1.Logger.error(`Query complexity of "${(_f = request.operationName) !== null && _f !== void 0 ? _f : "anonymous"}" is ${complexity}, which exceeds the maximum of ${maxQueryComplexity}`, constants_1.loggerCtx);
48
+ throw new core_1.InternalServerError("Query is too complex");
49
+ }
50
+ },
51
+ };
52
+ }
53
+ }
54
+ exports.QueryComplexityPlugin = QueryComplexityPlugin;
55
+ function isAdminApi(schema) {
56
+ const queryType = schema.getQueryType();
57
+ if (queryType) {
58
+ return !!queryType.getFields().administrators;
59
+ }
60
+ return false;
61
+ }
62
+ /**
63
+ * @description
64
+ * A complexity estimator which takes into account List and PaginatedList types and can
65
+ * be further configured by providing a customComplexityFactors object.
66
+ *
67
+ * When selecting PaginatedList types, the "take" argument is used to estimate a complexity
68
+ * factor. If the "take" argument is omitted, a default factor of 1000 is applied.
69
+ *
70
+ * @docsCategory core plugins/HardenPlugin
71
+ */
72
+ function defaultDeenruvComplexityEstimator(customComplexityFactors, logFieldScores) {
73
+ return (options) => {
74
+ var _a, _b;
75
+ const { type, args, childComplexity, field } = options;
76
+ const namedType = (0, graphql_1.getNamedType)(field.type);
77
+ const path = `${type.name}.${field.name}`;
78
+ let result = childComplexity + 1;
79
+ const customFactor = customComplexityFactors[path];
80
+ if (customFactor != null) {
81
+ result = Math.max(childComplexity, 1) * customFactor;
82
+ }
83
+ else {
84
+ if ((0, graphql_1.isObjectType)(namedType)) {
85
+ const isPaginatedList = !!namedType
86
+ .getInterfaces()
87
+ .find((i) => i.name === "PaginatedList");
88
+ if (isPaginatedList) {
89
+ const take = (_b = (_a = args.options) === null || _a === void 0 ? void 0 : _a.take) !== null && _b !== void 0 ? _b : 1000;
90
+ result =
91
+ childComplexity + Math.round(Math.log(childComplexity) * take);
92
+ }
93
+ }
94
+ if ((0, graphql_1.isListType)((0, graphql_1.getNullableType)(field.type))) {
95
+ result = childComplexity * 5;
96
+ }
97
+ }
98
+ if (logFieldScores) {
99
+ core_1.Logger.debug(`${path}: ${field.type.toString()}\tchildComplexity: ${childComplexity}, score: ${result}`, constants_1.loggerCtx);
100
+ }
101
+ return result;
102
+ };
103
+ }
104
+ exports.defaultDeenruvComplexityEstimator = defaultDeenruvComplexityEstimator;
105
+ //# sourceMappingURL=query-complexity-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-complexity-plugin.js","sourceRoot":"","sources":["../../../src/middleware/query-complexity-plugin.ts"],"names":[],"mappings":";;;AAKA,wCAA4D;AAC5D,qCAOiB;AACjB,uEAIkC;AAElC,4CAAyC;AAGzC;;;GAGG;AACH,MAAa,qBAAqB;IAChC,YAAoB,OAA4B;QAA5B,YAAO,GAAP,OAAO,CAAqB;IAAG,CAAC;IAEpD,KAAK,CAAC,eAAe,CAAC,EACpB,MAAM,GACqB;;QAC3B,MAAM,kBAAkB,GAAG,MAAA,IAAI,CAAC,OAAO,CAAC,kBAAkB,mCAAI,IAAI,CAAC;QACnE,OAAO;YACL,mBAAmB,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE;;gBACnD,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;oBACvB,kDAAkD;oBAClD,0DAA0D;oBAC1D,4BAA4B;oBAC5B,OAAO;gBACT,CAAC;gBACD,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa;oBACjC,CAAC,CAAC,IAAA,4BAAkB,EAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC;oBACrD,CAAC,CAAC,QAAQ,CAAC;gBAEb,IAAI,IAAI,CAAC,OAAO,CAAC,kBAAkB,KAAK,IAAI,EAAE,CAAC;oBAC7C,aAAM,CAAC,KAAK,CACV,8BAA8B,MAAA,OAAO,CAAC,aAAa,mCAAI,WAAW,GAAG,EACrE,qBAAS,CACV,CAAC;gBACJ,CAAC;gBACD,MAAM,UAAU,GAAG,IAAA,wCAAa,EAAC;oBAC/B,MAAM;oBACN,KAAK;oBACL,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,UAAU,EAAE,MAAA,IAAI,CAAC,OAAO,CAAC,yBAAyB,mCAAI;wBACpD,iCAAiC,CAC/B,MAAA,IAAI,CAAC,OAAO,CAAC,uBAAuB,mCAAI,EAAE,EAC1C,MAAA,IAAI,CAAC,OAAO,CAAC,kBAAkB,mCAAI,KAAK,CACzC;wBACD,IAAA,0CAAe,EAAC,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;qBAC1C;iBACF,CAAC,CAAC;gBAEH,IAAI,IAAI,CAAC,OAAO,CAAC,kBAAkB,KAAK,IAAI,EAAE,CAAC;oBAC7C,aAAM,CAAC,OAAO,CACZ,qBAAqB,MAAA,OAAO,CAAC,aAAa,mCAAI,WAAW,MAAM,UAAU,EAAE,EAC3E,qBAAS,CACV,CAAC;gBACJ,CAAC;gBACD,IAAI,UAAU,IAAI,kBAAkB,EAAE,CAAC;oBACrC,aAAM,CAAC,KAAK,CACV,wBACE,MAAA,OAAO,CAAC,aAAa,mCAAI,WAC3B,QAAQ,UAAU,kCAAkC,kBAAkB,EAAE,EACxE,qBAAS,CACV,CAAC;oBACF,MAAM,IAAI,0BAAmB,CAAC,sBAAsB,CAAC,CAAC;gBACxD,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF;AAxDD,sDAwDC;AAED,SAAS,UAAU,CAAC,MAAqB;IACvC,MAAM,SAAS,GAAG,MAAM,CAAC,YAAY,EAAE,CAAC;IACxC,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC,cAAc,CAAC;IAChD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,iCAAiC,CAC/C,uBAAmD,EACnD,cAAuB;IAEvB,OAAO,CAAC,OAAgC,EAAiB,EAAE;;QACzD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;QACvD,MAAM,SAAS,GAAG,IAAA,sBAAY,EAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAC1C,IAAI,MAAM,GAAG,eAAe,GAAG,CAAC,CAAC;QACjC,MAAM,YAAY,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC,GAAG,YAAY,CAAC;QACvD,CAAC;aAAM,CAAC;YACN,IAAI,IAAA,sBAAY,EAAC,SAAS,CAAC,EAAE,CAAC;gBAC5B,MAAM,eAAe,GAAG,CAAC,CAAC,SAAS;qBAChC,aAAa,EAAE;qBACf,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,eAAe,CAAC,CAAC;gBAC3C,IAAI,eAAe,EAAE,CAAC;oBACpB,MAAM,IAAI,GAAG,MAAA,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,mCAAI,IAAI,CAAC;oBACxC,MAAM;wBACJ,eAAe,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC,CAAC;gBACnE,CAAC;YACH,CAAC;YACD,IAAI,IAAA,oBAAU,EAAC,IAAA,yBAAe,EAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;gBAC5C,MAAM,GAAG,eAAe,GAAG,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QACD,IAAI,cAAc,EAAE,CAAC;YACnB,aAAM,CAAC,KAAK,CACV,GAAG,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,sBAAsB,eAAe,YAAY,MAAM,EAAE,EAC1F,qBAAS,CACV,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC;AAnCD,8EAmCC"}
@@ -0,0 +1,79 @@
1
+ import { ComplexityEstimator } from "graphql-query-complexity";
2
+ /**
3
+ * @description
4
+ * Options that can be passed to the `.init()` static method of the HardenPlugin.
5
+ *
6
+ * @docsCategory core plugins/HardenPlugin
7
+ */
8
+ export interface HardenPluginOptions {
9
+ /**
10
+ * @description
11
+ * Defines the maximum permitted complexity score of a query. The complexity score is based
12
+ * on the number of fields being selected as well as other factors like whether there are nested
13
+ * lists.
14
+ *
15
+ * A query which exceeds the maximum score will result in an error.
16
+ *
17
+ * @default 1000
18
+ */
19
+ maxQueryComplexity?: number;
20
+ /**
21
+ * @description
22
+ * An array of custom estimator functions for calculating the complexity of a query. By default,
23
+ * the plugin will use the {@link defaultDeenruvComplexityEstimator} which is specifically
24
+ * tuned to accurately estimate Deenruv queries.
25
+ */
26
+ queryComplexityEstimators?: ComplexityEstimator[];
27
+ /**
28
+ * @description
29
+ * When set to `true`, the complexity score of each query will be logged at the Verbose
30
+ * log level, and a breakdown of the calculation for each field will be logged at the Debug level.
31
+ *
32
+ * This is very useful for tuning your complexity scores.
33
+ *
34
+ * @default false
35
+ */
36
+ logComplexityScore?: boolean;
37
+ /**
38
+ * @description
39
+ * This object allows you to tune the complexity weight of specific fields. For example,
40
+ * if you have a custom `stockLocations` field defined on the `ProductVariant` type, and
41
+ * you know that it is a particularly expensive operation to execute, you can increase
42
+ * its complexity like this:
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * HardenPlugin.init({
47
+ * maxQueryComplexity: 650,
48
+ * customComplexityFactors: {
49
+ * 'ProductVariant.stockLocations': 10
50
+ * }
51
+ * }),
52
+ * ```
53
+ */
54
+ customComplexityFactors?: {
55
+ [path: string]: number;
56
+ };
57
+ /**
58
+ * @description
59
+ * Graphql-js will make suggestions about the names of fields if an invalid field name is provided.
60
+ * This would allow an attacker to find out the available fields by brute force even if introspection
61
+ * is disabled.
62
+ *
63
+ * Setting this option to `true` will prevent these suggestion error messages from being returned,
64
+ * instead replacing the message with a generic "Invalid request" message.
65
+ *
66
+ * @default true
67
+ */
68
+ hideFieldSuggestions?: boolean;
69
+ /**
70
+ * @description
71
+ * When set to `'prod'`, the plugin will disable dev-mode features of the GraphQL APIs:
72
+ *
73
+ * - introspection
74
+ * - GraphQL playground
75
+ *
76
+ * @default 'prod'
77
+ */
78
+ apiMode?: "dev" | "prod";
79
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@deenruv/harden-plugin",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "files": [
8
+ "lib/**/*"
9
+ ],
10
+ "homepage": "https://deenruv.com/",
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "peerDependencies": {
15
+ "@deenruv/core": "^0.1.0"
16
+ },
17
+ "dependencies": {
18
+ "@apollo/server": "^4.10.4",
19
+ "graphql-query-complexity": "^0.12.0"
20
+ },
21
+ "devDependencies": {
22
+ "rimraf": "^5.0.5",
23
+ "@deenruv/core": "^1.0.0",
24
+ "@deenruv/common": "^1.0.0"
25
+ },
26
+ "scripts": {
27
+ "watch": "tsc -p ./tsconfig.build.json --watch",
28
+ "build": "rimraf lib && tsc -p ./tsconfig.build.json",
29
+ "lint": "eslint .",
30
+ "lint:fix": "eslint --fix ."
31
+ }
32
+ }