opal-security 3.1.0 → 3.1.1-beta.16f5ed5
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 +18 -34
- package/lib/commands/request/create.js +10 -8
- package/lib/commands/request/get.d.ts +7 -1
- package/lib/commands/request/get.js +106 -5
- package/lib/commands/request/list.d.ts +8 -2
- package/lib/commands/request/list.js +113 -6
- package/lib/graphql/gql.d.ts +32 -2
- package/lib/graphql/gql.js +7 -1
- package/lib/graphql/graphql.d.ts +359 -117
- package/lib/graphql/graphql.js +1635 -173
- package/lib/lib/requests.d.ts +34 -10
- package/lib/lib/requests.js +650 -109
- package/lib/utils/displays.d.ts +8 -3
- package/lib/utils/displays.js +184 -42
- package/oclif.manifest.json +70 -8
- package/package.json +4 -1
package/lib/utils/displays.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import type { ApolloQueryResult } from "@apollo/client";
|
|
1
2
|
import type { Command } from "@oclif/core/lib/command";
|
|
2
|
-
import type {
|
|
3
|
+
import type { GetRequestQuery, GetRequestsQuery } from "../graphql/graphql";
|
|
4
|
+
import type { RequestMap, RequestMetadata } from "../lib/requests";
|
|
3
5
|
export declare function headerMessage(cmd: Command): void;
|
|
4
|
-
export declare function treeifyRequestMap(requestMap: RequestMap):
|
|
5
|
-
export declare function displayFinalRequestSummary(cmd: Command,
|
|
6
|
+
export declare function treeifyRequestMap(cmd: Command, requestMap: RequestMap): void;
|
|
7
|
+
export declare function displayFinalRequestSummary(cmd: Command, metadata: RequestMetadata): void;
|
|
8
|
+
export declare function getStyledStatus(status: string): string;
|
|
9
|
+
export declare function displayRequestDetails(cmd: Command, requestResp: ApolloQueryResult<GetRequestQuery>): void;
|
|
10
|
+
export declare function displayRequestListTable(cmd: Command, requestResp: ApolloQueryResult<GetRequestsQuery>): void;
|
package/lib/utils/displays.js
CHANGED
|
@@ -3,63 +3,205 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.headerMessage = headerMessage;
|
|
4
4
|
exports.treeifyRequestMap = treeifyRequestMap;
|
|
5
5
|
exports.displayFinalRequestSummary = displayFinalRequestSummary;
|
|
6
|
-
|
|
6
|
+
exports.getStyledStatus = getStyledStatus;
|
|
7
|
+
exports.displayRequestDetails = displayRequestDetails;
|
|
8
|
+
exports.displayRequestListTable = displayRequestListTable;
|
|
9
|
+
const chalk_1 = require("chalk");
|
|
7
10
|
const Table = require("cli-table3");
|
|
8
|
-
const
|
|
9
|
-
top: "═",
|
|
10
|
-
"top-mid": "╤",
|
|
11
|
-
"top-left": "╔",
|
|
12
|
-
"top-right": "╗",
|
|
13
|
-
bottom: "═",
|
|
14
|
-
"bottom-mid": "╧",
|
|
15
|
-
"bottom-left": "╚",
|
|
16
|
-
"bottom-right": "╝",
|
|
17
|
-
left: "║",
|
|
18
|
-
"left-mid": "╟",
|
|
19
|
-
mid: "─",
|
|
20
|
-
"mid-mid": "┼",
|
|
21
|
-
right: "║",
|
|
22
|
-
"right-mid": "╢",
|
|
23
|
-
middle: "│",
|
|
24
|
-
};
|
|
11
|
+
const treeify = require("object-treeify").default;
|
|
25
12
|
function headerMessage(cmd) {
|
|
26
13
|
console.clear();
|
|
27
|
-
cmd.log("
|
|
14
|
+
cmd.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
28
15
|
cmd.log("Opal Access Request ✏️");
|
|
29
16
|
cmd.log("Press Ctrl+C to cancel at any time.\n");
|
|
30
17
|
}
|
|
31
|
-
function treeifyRequestMap(requestMap) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
18
|
+
function treeifyRequestMap(cmd, requestMap) {
|
|
19
|
+
// Configuration options for treeify
|
|
20
|
+
const options = {
|
|
21
|
+
joined: true,
|
|
22
|
+
spacerNoNeighbour: " ",
|
|
23
|
+
spacerNeighbour: "│ ",
|
|
24
|
+
keyNoNeighbour: "└── ",
|
|
25
|
+
keyNeighbour: "├── ",
|
|
26
|
+
separator: "",
|
|
27
|
+
};
|
|
28
|
+
for (const [_appId, appNode] of Object.entries(requestMap)) {
|
|
29
|
+
const assetsTree = {};
|
|
30
|
+
for (const [_assetId, assetNode] of Object.entries(appNode.assets)) {
|
|
31
|
+
const assetKey = `${assetNode.assetName} [${assetNode.type}]`;
|
|
41
32
|
if (assetNode.roles !== undefined) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
requestTree[appKey][assetKey][roleName] = null; // Initialize the role key
|
|
33
|
+
assetsTree[assetKey] = {};
|
|
34
|
+
for (const [_roleId, roleNode] of Object.entries(assetNode.roles)) {
|
|
35
|
+
const roleKey = `${roleNode.roleName} [Role]`;
|
|
36
|
+
assetsTree[assetKey][roleKey] = null;
|
|
47
37
|
}
|
|
48
38
|
}
|
|
49
39
|
else {
|
|
50
|
-
|
|
40
|
+
assetsTree[assetKey] = null;
|
|
51
41
|
}
|
|
52
42
|
}
|
|
43
|
+
// Render tree for this app's assets
|
|
44
|
+
const assetsTreeString = treeify(assetsTree, options);
|
|
45
|
+
// Print App title first (without tree lines)
|
|
46
|
+
cmd.log(`${chalk_1.default.bold(appNode.appName)} [App]`);
|
|
47
|
+
// Print its assets/roles indented underneath
|
|
48
|
+
cmd.log(assetsTreeString);
|
|
53
49
|
}
|
|
54
|
-
|
|
50
|
+
cmd.log("\n");
|
|
55
51
|
}
|
|
56
|
-
function displayFinalRequestSummary(cmd,
|
|
52
|
+
function displayFinalRequestSummary(cmd, metadata) {
|
|
57
53
|
headerMessage(cmd);
|
|
58
54
|
cmd.log("Final Summary of Request\n");
|
|
59
|
-
const requestedAssets = treeifyRequestMap(requestMap);
|
|
60
|
-
const table = new Table(
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
table.push(["Requested Assets", requestedAssets], ["Reason", reason], ["Expiration", expiration]);
|
|
55
|
+
const requestedAssets = treeifyRequestMap(cmd, metadata.requestMap);
|
|
56
|
+
const table = new Table();
|
|
57
|
+
table.push(["Requested Assets", requestedAssets], ["Reason", metadata.reason], ["Expiration", metadata.durationLabel]);
|
|
64
58
|
cmd.log(table.toString());
|
|
65
59
|
}
|
|
60
|
+
function getStyledStatus(status) {
|
|
61
|
+
switch (status) {
|
|
62
|
+
case "PENDING": {
|
|
63
|
+
return `${chalk_1.default.bold("Status:")} ${chalk_1.default.blueBright(status)}`;
|
|
64
|
+
}
|
|
65
|
+
case "APPROVED": {
|
|
66
|
+
return `${chalk_1.default.bold("Status:")} ${chalk_1.default.greenBright(status)}`;
|
|
67
|
+
}
|
|
68
|
+
case "DENIED": {
|
|
69
|
+
return `${chalk_1.default.bold("Status:")} ${chalk_1.default.redBright(status)}`;
|
|
70
|
+
}
|
|
71
|
+
case "CANCELED": {
|
|
72
|
+
return `${chalk_1.default.bold("Status:")} ${chalk_1.default.redBright(status)}`;
|
|
73
|
+
}
|
|
74
|
+
default: {
|
|
75
|
+
return `${chalk_1.default.bold("Status:")} ${chalk_1.default.gray(status)}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function displayRequestDetails(cmd, requestResp) {
|
|
80
|
+
var _a, _b, _c, _d, _e, _f;
|
|
81
|
+
switch (requestResp.data.request.__typename) {
|
|
82
|
+
case "RequestResult": {
|
|
83
|
+
cmd.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
84
|
+
cmd.log(`Request Details ${chalk_1.default.cyan(requestResp.data.request.request.id)}`);
|
|
85
|
+
cmd.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
86
|
+
const status = requestResp.data.request.request.status;
|
|
87
|
+
cmd.log(getStyledStatus(status));
|
|
88
|
+
// Request users "Requested by: <requester> -> Requested for: <targetUser>"
|
|
89
|
+
const requester = (_a = requestResp.data.request.request.requester) === null || _a === void 0 ? void 0 : _a.displayName;
|
|
90
|
+
const targetUser = (_b = requestResp.data.request.request.targetUser) === null || _b === void 0 ? void 0 : _b.displayName;
|
|
91
|
+
if (requester && targetUser) {
|
|
92
|
+
cmd.log(`${chalk_1.default.bold("Requested by:")} ${requester} ${chalk_1.default.gray("->")} ${chalk_1.default.bold("Requested for:")} ${targetUser}`);
|
|
93
|
+
}
|
|
94
|
+
const durationInMinutes = requestResp.data.request.request.durationInMinutes;
|
|
95
|
+
cmd.log(`${chalk_1.default.bold("Duration:")} ${durationInMinutes ? formatDuration(durationInMinutes) : "Permanent"}`);
|
|
96
|
+
const reason = requestResp.data.request.request.reason;
|
|
97
|
+
if (reason) {
|
|
98
|
+
cmd.log(`${chalk_1.default.bold("Reason:")} "${chalk_1.default.italic(reason)}"`);
|
|
99
|
+
}
|
|
100
|
+
// Requested resources
|
|
101
|
+
const requestedResources = (_d = (_c = requestResp.data.request.request.requestedResources) === null || _c === void 0 ? void 0 : _c.map((resource) => {
|
|
102
|
+
var _a, _b, _c;
|
|
103
|
+
if (((_a = resource.resource) === null || _a === void 0 ? void 0 : _a.__typename) === "Resource") {
|
|
104
|
+
return formatAssetName((_b = resource.resource) === null || _b === void 0 ? void 0 : _b.displayName, ((_c = resource.accessLevel) === null || _c === void 0 ? void 0 : _c.accessLevelName) || "");
|
|
105
|
+
}
|
|
106
|
+
})) !== null && _d !== void 0 ? _d : [];
|
|
107
|
+
const requestedGroups = (_f = (_e = requestResp.data.request.request.requestedGroups) === null || _e === void 0 ? void 0 : _e.map((group) => {
|
|
108
|
+
var _a, _b, _c;
|
|
109
|
+
if (((_a = group.group) === null || _a === void 0 ? void 0 : _a.__typename) === "Group") {
|
|
110
|
+
return formatAssetName((_b = group.group) === null || _b === void 0 ? void 0 : _b.name, ((_c = group.accessLevel) === null || _c === void 0 ? void 0 : _c.accessLevelName) || "");
|
|
111
|
+
}
|
|
112
|
+
})) !== null && _f !== void 0 ? _f : [];
|
|
113
|
+
const requestedItems = [...requestedResources, ...requestedGroups].join(", ");
|
|
114
|
+
if (requestedItems) {
|
|
115
|
+
cmd.log(`${chalk_1.default.bold("Requested Items:")} ${chalk_1.default.cyan(requestedItems)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function displayRequestListTable(cmd, requestResp) {
|
|
121
|
+
var _a, _b, _c, _d, _e;
|
|
122
|
+
switch (requestResp.data.requests.__typename) {
|
|
123
|
+
case "RequestsResult": {
|
|
124
|
+
const requests = requestResp.data.requests.requests;
|
|
125
|
+
if (requests && requests.length > 0) {
|
|
126
|
+
const table = new Table({
|
|
127
|
+
head: [
|
|
128
|
+
"Request ID",
|
|
129
|
+
"Status",
|
|
130
|
+
"For",
|
|
131
|
+
"Duration",
|
|
132
|
+
"Requested Items",
|
|
133
|
+
"Reason",
|
|
134
|
+
],
|
|
135
|
+
colWidths: [null, null, 20, null, 30, 20],
|
|
136
|
+
wordWrap: true,
|
|
137
|
+
wrapOnWordBoundary: false,
|
|
138
|
+
});
|
|
139
|
+
for (const request of requests) {
|
|
140
|
+
const targetUser = (_a = request.targetUser) === null || _a === void 0 ? void 0 : _a.displayName;
|
|
141
|
+
const reason = request.reason;
|
|
142
|
+
const status = request.status;
|
|
143
|
+
let formattedDuration = "";
|
|
144
|
+
if (request.durationInMinutes) {
|
|
145
|
+
formattedDuration = formatDuration(request.durationInMinutes);
|
|
146
|
+
}
|
|
147
|
+
const requestedResources = (_c = (_b = request.requestedResources) === null || _b === void 0 ? void 0 : _b.map((resource) => {
|
|
148
|
+
var _a, _b, _c;
|
|
149
|
+
if (((_a = resource.resource) === null || _a === void 0 ? void 0 : _a.__typename) === "Resource") {
|
|
150
|
+
return formatAssetName((_b = resource.resource) === null || _b === void 0 ? void 0 : _b.displayName, ((_c = resource.accessLevel) === null || _c === void 0 ? void 0 : _c.accessLevelName) || "");
|
|
151
|
+
}
|
|
152
|
+
})) !== null && _c !== void 0 ? _c : [];
|
|
153
|
+
const requestedGroups = (_e = (_d = request.requestedGroups) === null || _d === void 0 ? void 0 : _d.map((group) => {
|
|
154
|
+
var _a, _b, _c;
|
|
155
|
+
if (((_a = group.group) === null || _a === void 0 ? void 0 : _a.__typename) === "Group") {
|
|
156
|
+
return formatAssetName((_b = group.group) === null || _b === void 0 ? void 0 : _b.name, ((_c = group.accessLevel) === null || _c === void 0 ? void 0 : _c.accessLevelName) || "");
|
|
157
|
+
}
|
|
158
|
+
})) !== null && _e !== void 0 ? _e : [];
|
|
159
|
+
const requestedItems = [
|
|
160
|
+
...requestedResources,
|
|
161
|
+
...requestedGroups,
|
|
162
|
+
].join(", ");
|
|
163
|
+
table.push([
|
|
164
|
+
request.id,
|
|
165
|
+
status,
|
|
166
|
+
targetUser,
|
|
167
|
+
formattedDuration,
|
|
168
|
+
requestedItems,
|
|
169
|
+
reason,
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
cmd.log(table.toString());
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
cmd.log("No requests found.");
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
default: {
|
|
180
|
+
cmd.log("No requests found.");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function formatDuration(durationInMinutes) {
|
|
185
|
+
const days = Math.floor(durationInMinutes / 1440);
|
|
186
|
+
const remainingMinutes = durationInMinutes % 1440;
|
|
187
|
+
const hours = Math.floor(remainingMinutes / 60);
|
|
188
|
+
const minutes = remainingMinutes % 60;
|
|
189
|
+
let durationStr = "";
|
|
190
|
+
if (days > 0)
|
|
191
|
+
durationStr += `${days}d`;
|
|
192
|
+
if (hours > 0)
|
|
193
|
+
durationStr += `${hours}h`;
|
|
194
|
+
if (minutes > 0)
|
|
195
|
+
durationStr += `${minutes}m`;
|
|
196
|
+
if (durationStr === "")
|
|
197
|
+
durationStr = `${durationInMinutes}m`;
|
|
198
|
+
durationStr += ` (${durationInMinutes}m)`;
|
|
199
|
+
return durationStr;
|
|
200
|
+
}
|
|
201
|
+
function formatAssetName(assetName, roleName) {
|
|
202
|
+
let str = `${assetName}`;
|
|
203
|
+
if (roleName) {
|
|
204
|
+
str += ` (${roleName})`;
|
|
205
|
+
}
|
|
206
|
+
return str;
|
|
207
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -597,7 +597,7 @@
|
|
|
597
597
|
"request:create": {
|
|
598
598
|
"aliases": [],
|
|
599
599
|
"args": {},
|
|
600
|
-
"description": "
|
|
600
|
+
"description": "Creates an Opal access request via an interactive form",
|
|
601
601
|
"flags": {},
|
|
602
602
|
"hasDynamicHelp": false,
|
|
603
603
|
"hidden": true,
|
|
@@ -620,7 +620,34 @@
|
|
|
620
620
|
"aliases": [],
|
|
621
621
|
"args": {},
|
|
622
622
|
"description": "Lists access requests",
|
|
623
|
-
"
|
|
623
|
+
"examples": [
|
|
624
|
+
"opal request get --id 54052a3e-5375-4392-aeaf-0c6c44c131d4",
|
|
625
|
+
"opal request get --id 54052a3e-5375-4392-aeaf-0c6c44c131d4 --verbose"
|
|
626
|
+
],
|
|
627
|
+
"flags": {
|
|
628
|
+
"help": {
|
|
629
|
+
"char": "h",
|
|
630
|
+
"description": "Show CLI help.",
|
|
631
|
+
"name": "help",
|
|
632
|
+
"allowNo": false,
|
|
633
|
+
"type": "boolean"
|
|
634
|
+
},
|
|
635
|
+
"id": {
|
|
636
|
+
"char": "i",
|
|
637
|
+
"description": "The Opal ID of the resource. You can find this from the URL, e.g. https://opal.dev/resources/[ID]",
|
|
638
|
+
"name": "id",
|
|
639
|
+
"hasDynamicHelp": false,
|
|
640
|
+
"multiple": false,
|
|
641
|
+
"type": "option"
|
|
642
|
+
},
|
|
643
|
+
"verbose": {
|
|
644
|
+
"char": "v",
|
|
645
|
+
"description": "Enable verbose output, prints full response in JSON format. Defaults to false.",
|
|
646
|
+
"name": "verbose",
|
|
647
|
+
"allowNo": false,
|
|
648
|
+
"type": "boolean"
|
|
649
|
+
}
|
|
650
|
+
},
|
|
624
651
|
"hasDynamicHelp": false,
|
|
625
652
|
"hidden": true,
|
|
626
653
|
"hiddenAliases": [],
|
|
@@ -639,12 +666,47 @@
|
|
|
639
666
|
]
|
|
640
667
|
},
|
|
641
668
|
"request:list": {
|
|
642
|
-
"aliases": [
|
|
643
|
-
"request:ls"
|
|
644
|
-
],
|
|
669
|
+
"aliases": [],
|
|
645
670
|
"args": {},
|
|
646
|
-
"description": "Lists access requests",
|
|
647
|
-
"
|
|
671
|
+
"description": "Lists your n recent outgoing access requests",
|
|
672
|
+
"examples": [
|
|
673
|
+
"opal request list --n 5",
|
|
674
|
+
"opal request list --n 5 --pending",
|
|
675
|
+
"opal request list --n 5 --verbose",
|
|
676
|
+
"opal request list --n 5 --pending --verbose"
|
|
677
|
+
],
|
|
678
|
+
"flags": {
|
|
679
|
+
"help": {
|
|
680
|
+
"char": "h",
|
|
681
|
+
"description": "Show CLI help.",
|
|
682
|
+
"name": "help",
|
|
683
|
+
"allowNo": false,
|
|
684
|
+
"type": "boolean"
|
|
685
|
+
},
|
|
686
|
+
"n": {
|
|
687
|
+
"char": "n",
|
|
688
|
+
"description": "Defines number of requests to be returned. 1 <= n <= 100.",
|
|
689
|
+
"name": "n",
|
|
690
|
+
"default": 10,
|
|
691
|
+
"hasDynamicHelp": false,
|
|
692
|
+
"multiple": false,
|
|
693
|
+
"type": "option"
|
|
694
|
+
},
|
|
695
|
+
"pending": {
|
|
696
|
+
"char": "p",
|
|
697
|
+
"description": "Show only pending requests. Defaults to false.",
|
|
698
|
+
"name": "pending",
|
|
699
|
+
"allowNo": false,
|
|
700
|
+
"type": "boolean"
|
|
701
|
+
},
|
|
702
|
+
"verbose": {
|
|
703
|
+
"char": "v",
|
|
704
|
+
"description": "Enable verbose output, prints full response in JSON format. Defaults to false.",
|
|
705
|
+
"name": "verbose",
|
|
706
|
+
"allowNo": false,
|
|
707
|
+
"type": "boolean"
|
|
708
|
+
}
|
|
709
|
+
},
|
|
648
710
|
"hasDynamicHelp": false,
|
|
649
711
|
"hidden": true,
|
|
650
712
|
"hiddenAliases": [],
|
|
@@ -909,5 +971,5 @@
|
|
|
909
971
|
]
|
|
910
972
|
}
|
|
911
973
|
},
|
|
912
|
-
"version": "3.1.
|
|
974
|
+
"version": "3.1.1-beta.16f5ed5"
|
|
913
975
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opal-security",
|
|
3
3
|
"description": "Opal allows you to centrally manage access to all of your sensitive systems.",
|
|
4
|
-
"version": "3.1.
|
|
4
|
+
"version": "3.1.1-beta.16f5ed5",
|
|
5
5
|
"author": "Stephen Cobbe",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opal": "./bin/run"
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"argon2": "^0.40.1",
|
|
17
17
|
"chalk": "^2.4.2",
|
|
18
18
|
"cli-table3": "^0.6.5",
|
|
19
|
+
"enquirer": "^2.4.1",
|
|
19
20
|
"graphql": "^15.5.0",
|
|
20
21
|
"inquirer": "^8.2.6",
|
|
21
22
|
"inquirer-autocomplete-prompt": "^2.0.1",
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"lodash": "^4.17.21",
|
|
24
25
|
"moment": "^2.30.1",
|
|
25
26
|
"node-fetch": "^2.6.7",
|
|
27
|
+
"object-treeify": "^5.0.1",
|
|
26
28
|
"open": "^8.0.4",
|
|
27
29
|
"openid-client": "^5.6.5",
|
|
28
30
|
"prettyjson": "^1.2.1",
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
"@types/lodash": "^4.14.169",
|
|
40
42
|
"@types/node": "^22.14.0",
|
|
41
43
|
"@types/prettyjson": "0.0.29",
|
|
44
|
+
"@types/react": "^19.1.4",
|
|
42
45
|
"@types/semver": "^7.3.8",
|
|
43
46
|
"better-npm-audit": "^3.7.3",
|
|
44
47
|
"get-graphql-schema": "^2.1.2",
|