@unchainedshop/api 4.8.12 → 4.8.14
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/lib/adminUiTheme.d.ts +25 -0
- package/lib/adminUiTheme.js +34 -0
- package/lib/express/createMCPMiddleware.js +24 -5
- package/lib/fastify/mcpHandler.js +22 -6
- package/lib/loaders/cartLoader.d.ts +13 -0
- package/lib/loaders/cartLoader.js +8 -0
- package/lib/loaders/index.d.ts +9 -0
- package/lib/loaders/index.js +2 -0
- package/lib/middleware/createAuthMiddleware.js +1 -3
- package/lib/resolvers/mutations/index.js +3 -3
- package/lib/resolvers/type/user-types.js +2 -2
- package/lib/roles/index.js +2 -0
- package/lib/roles/loggedIn.js +10 -0
- package/package.json +6 -6
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface AdminUIThemeTokens {
|
|
2
|
+
surface?: string;
|
|
3
|
+
'surface-subtle'?: string;
|
|
4
|
+
'surface-raised'?: string;
|
|
5
|
+
'surface-input'?: string;
|
|
6
|
+
border?: string;
|
|
7
|
+
'border-subtle'?: string;
|
|
8
|
+
'text-primary'?: string;
|
|
9
|
+
'text-secondary'?: string;
|
|
10
|
+
'text-muted'?: string;
|
|
11
|
+
'text-on-dark'?: string;
|
|
12
|
+
accent?: string;
|
|
13
|
+
'accent-hover'?: string;
|
|
14
|
+
danger?: string;
|
|
15
|
+
'danger-surface'?: string;
|
|
16
|
+
success?: string;
|
|
17
|
+
warning?: string;
|
|
18
|
+
'focus-ring'?: string;
|
|
19
|
+
'text-on-accent'?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AdminUIThemeConfig {
|
|
22
|
+
light?: AdminUIThemeTokens;
|
|
23
|
+
dark?: AdminUIThemeTokens;
|
|
24
|
+
}
|
|
25
|
+
export declare const generateThemeCSS: (theme?: AdminUIThemeConfig) => string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const CSS_VALUE_RE = /^[a-zA-Z0-9#().,/%\s\-_]+$/;
|
|
2
|
+
const sanitizeCSSValue = (value) => {
|
|
3
|
+
const trimmed = value.trim();
|
|
4
|
+
if (!trimmed || !CSS_VALUE_RE.test(trimmed))
|
|
5
|
+
return null;
|
|
6
|
+
return trimmed;
|
|
7
|
+
};
|
|
8
|
+
const buildTokenBlock = (selector, tokens) => {
|
|
9
|
+
const vars = Object.entries(tokens)
|
|
10
|
+
.map(([key, value]) => {
|
|
11
|
+
const safe = sanitizeCSSValue(value);
|
|
12
|
+
return safe ? ` --token-${key}: ${safe};` : null;
|
|
13
|
+
})
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
if (vars.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
return `${selector} {\n${vars.join('\n')}\n}`;
|
|
18
|
+
};
|
|
19
|
+
export const generateThemeCSS = (theme) => {
|
|
20
|
+
if (!theme)
|
|
21
|
+
return '/* default theme */';
|
|
22
|
+
const blocks = [];
|
|
23
|
+
if (theme.light) {
|
|
24
|
+
const block = buildTokenBlock(':root:root', theme.light);
|
|
25
|
+
if (block)
|
|
26
|
+
blocks.push(block);
|
|
27
|
+
}
|
|
28
|
+
if (theme.dark) {
|
|
29
|
+
const block = buildTokenBlock('.dark.dark', theme.dark);
|
|
30
|
+
if (block)
|
|
31
|
+
blocks.push(block);
|
|
32
|
+
}
|
|
33
|
+
return blocks.length > 0 ? blocks.join('\n\n') : '/* default theme */';
|
|
34
|
+
};
|
|
@@ -17,15 +17,27 @@ catch {
|
|
|
17
17
|
const transports = {};
|
|
18
18
|
const handlePostRequest = async (req, res) => {
|
|
19
19
|
const sessionId = req.headers['mcp-session-id'];
|
|
20
|
+
const currentUserId = req.unchainedContext.user._id;
|
|
20
21
|
let transport;
|
|
21
22
|
if (sessionId && transports[sessionId]) {
|
|
22
|
-
|
|
23
|
+
if (transports[sessionId].userId !== currentUserId) {
|
|
24
|
+
res.status(404).json({
|
|
25
|
+
jsonrpc: '2.0',
|
|
26
|
+
error: {
|
|
27
|
+
code: -32000,
|
|
28
|
+
message: 'Bad Request: No valid session ID provided',
|
|
29
|
+
},
|
|
30
|
+
id: null,
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
transport = transports[sessionId].transport;
|
|
23
35
|
}
|
|
24
36
|
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
25
37
|
transport = new StreamableHTTPServerTransport({
|
|
26
38
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
27
39
|
onsessioninitialized: (sessionId) => {
|
|
28
|
-
transports[sessionId] = transport;
|
|
40
|
+
transports[sessionId] = { transport, userId: currentUserId };
|
|
29
41
|
},
|
|
30
42
|
});
|
|
31
43
|
transport.onclose = () => {
|
|
@@ -56,15 +68,18 @@ const handlePostRequest = async (req, res) => {
|
|
|
56
68
|
};
|
|
57
69
|
const handleSessionRequest = async (req, res) => {
|
|
58
70
|
const sessionId = req.headers['mcp-session-id'];
|
|
59
|
-
if (!sessionId ||
|
|
71
|
+
if (!sessionId ||
|
|
72
|
+
!transports[sessionId] ||
|
|
73
|
+
transports[sessionId].userId !== req.unchainedContext.user._id) {
|
|
60
74
|
res.status(400).send('Invalid or missing session ID');
|
|
61
75
|
return;
|
|
62
76
|
}
|
|
63
|
-
const transport = transports[sessionId];
|
|
77
|
+
const transport = transports[sessionId].transport;
|
|
64
78
|
await transport.handleRequest(req, res);
|
|
65
79
|
};
|
|
66
80
|
const createMCPMiddleware = (req, res, next) => {
|
|
67
|
-
|
|
81
|
+
const user = req.unchainedContext.user;
|
|
82
|
+
if (!user) {
|
|
68
83
|
res.status(401);
|
|
69
84
|
res.header('WWW-Authenticate', `Bearer realm="Unchained MCP", error="invalid_token", resource="${process.env.ROOT_URL || 'http://localhost:4010'}",`);
|
|
70
85
|
res.json({
|
|
@@ -73,6 +88,10 @@ const createMCPMiddleware = (req, res, next) => {
|
|
|
73
88
|
});
|
|
74
89
|
return;
|
|
75
90
|
}
|
|
91
|
+
if (!(user.roles || []).includes('admin')) {
|
|
92
|
+
res.status(403).json({ error: 'forbidden', message: 'MCP requires admin privileges' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
76
95
|
if (req.method === 'POST') {
|
|
77
96
|
return handlePostRequest(req, res, next);
|
|
78
97
|
}
|
|
@@ -16,7 +16,8 @@ catch {
|
|
|
16
16
|
}
|
|
17
17
|
const transports = {};
|
|
18
18
|
const mcpHandler = async (req, res) => {
|
|
19
|
-
|
|
19
|
+
const user = req.unchainedContext.user;
|
|
20
|
+
if (!user) {
|
|
20
21
|
res.status(401);
|
|
21
22
|
res.header('WWW-Authenticate', `Bearer realm="Unchained MCP", error="invalid_token", resource="${process.env.ROOT_URL || 'http://localhost:4010'}",`);
|
|
22
23
|
return res.send(JSON.stringify({
|
|
@@ -24,18 +25,33 @@ const mcpHandler = async (req, res) => {
|
|
|
24
25
|
resource_metadata: `${process.env.ROOT_URL || 'http://localhost:4010'}/.well-known/oauth-protected-resource`,
|
|
25
26
|
}));
|
|
26
27
|
}
|
|
28
|
+
if (!(user.roles || []).includes('admin')) {
|
|
29
|
+
res.status(403);
|
|
30
|
+
return res.send(JSON.stringify({ error: 'forbidden', message: 'MCP requires admin privileges' }));
|
|
31
|
+
}
|
|
32
|
+
const currentUserId = user._id;
|
|
27
33
|
try {
|
|
28
34
|
if (req.method === 'POST') {
|
|
29
35
|
const sessionId = req.headers['mcp-session-id'];
|
|
30
36
|
let transport;
|
|
31
37
|
if (sessionId && transports[sessionId]) {
|
|
32
|
-
|
|
38
|
+
if (transports[sessionId].userId !== currentUserId) {
|
|
39
|
+
return res.status(404).send(JSON.stringify({
|
|
40
|
+
jsonrpc: '2.0',
|
|
41
|
+
error: {
|
|
42
|
+
code: -32000,
|
|
43
|
+
message: 'Bad Request: No valid session ID provided',
|
|
44
|
+
},
|
|
45
|
+
id: null,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
transport = transports[sessionId].transport;
|
|
33
49
|
}
|
|
34
50
|
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
35
51
|
transport = new StreamableHTTPServerTransport({
|
|
36
52
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
37
53
|
onsessioninitialized: (sessionId) => {
|
|
38
|
-
transports[sessionId] = transport;
|
|
54
|
+
transports[sessionId] = { transport, userId: currentUserId };
|
|
39
55
|
},
|
|
40
56
|
});
|
|
41
57
|
transport.onclose = () => {
|
|
@@ -43,7 +59,7 @@ const mcpHandler = async (req, res) => {
|
|
|
43
59
|
delete transports[transport.sessionId];
|
|
44
60
|
}
|
|
45
61
|
};
|
|
46
|
-
const roles =
|
|
62
|
+
const roles = user.roles || [];
|
|
47
63
|
const { default: initMCPServer } = await import("../mcp/index.js");
|
|
48
64
|
const server = initMCPServer(new McpServer({
|
|
49
65
|
name: 'Unchained MCP Server',
|
|
@@ -66,10 +82,10 @@ const mcpHandler = async (req, res) => {
|
|
|
66
82
|
}
|
|
67
83
|
else if (req.method === 'GET' || req.method === 'DELETE') {
|
|
68
84
|
const sessionId = req.headers['mcp-session-id'];
|
|
69
|
-
if (!sessionId || !transports[sessionId]) {
|
|
85
|
+
if (!sessionId || !transports[sessionId] || transports[sessionId].userId !== currentUserId) {
|
|
70
86
|
return res.status(400).send('Invalid or missing session ID');
|
|
71
87
|
}
|
|
72
|
-
const transport = transports[sessionId];
|
|
88
|
+
const transport = transports[sessionId].transport;
|
|
73
89
|
await transport.handleRequest(req.raw, res.raw);
|
|
74
90
|
return res;
|
|
75
91
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { UnchainedCore } from '@unchainedshop/core';
|
|
2
|
+
import type { Order } from '@unchainedshop/core-orders';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
|
|
5
|
+
userId: string;
|
|
6
|
+
countryCode?: string;
|
|
7
|
+
orderNumber?: string;
|
|
8
|
+
}, Order | null, {
|
|
9
|
+
userId: string;
|
|
10
|
+
countryCode?: string;
|
|
11
|
+
orderNumber?: string;
|
|
12
|
+
}>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import DataLoader from 'dataloader';
|
|
2
|
+
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
|
+
const userIds = [...new Set(queries.map((q) => q.userId).filter(Boolean))];
|
|
4
|
+
const carts = await unchainedAPI.modules.orders.findCarts({ userIds });
|
|
5
|
+
return queries.map((q) => carts.find((cart) => cart.userId === q.userId &&
|
|
6
|
+
(!q.countryCode || cart.countryCode === q.countryCode) &&
|
|
7
|
+
(!q.orderNumber || cart.orderNumber === q.orderNumber)) || null);
|
|
8
|
+
});
|
package/lib/loaders/index.d.ts
CHANGED
|
@@ -166,6 +166,15 @@ declare const loaders: (unchainedAPI: UnchainedCore) => {
|
|
|
166
166
|
}, import("@unchainedshop/core-orders").Order, {
|
|
167
167
|
orderId: string;
|
|
168
168
|
}>;
|
|
169
|
+
cartLoader: import("dataloader")<{
|
|
170
|
+
userId: string;
|
|
171
|
+
countryCode?: string;
|
|
172
|
+
orderNumber?: string;
|
|
173
|
+
}, import("@unchainedshop/core-orders").Order | null, {
|
|
174
|
+
userId: string;
|
|
175
|
+
countryCode?: string;
|
|
176
|
+
orderNumber?: string;
|
|
177
|
+
}>;
|
|
169
178
|
orderPaymentLoader: import("dataloader")<{
|
|
170
179
|
orderPaymentId: string;
|
|
171
180
|
}, import("@unchainedshop/core-orders").OrderPayment | null, {
|
package/lib/loaders/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import deliveryProviderLoader from "./deliveryProviderLoader.js";
|
|
|
26
26
|
import paymentProviderLoader from "./paymentProviderLoader.js";
|
|
27
27
|
import warehousingProviderLoader from "./warehousingProviderLoader.js";
|
|
28
28
|
import orderLoader from "./orderLoader.js";
|
|
29
|
+
import cartLoader from "./cartLoader.js";
|
|
29
30
|
import orderPaymentLoader from "./orderPaymentLoader.js";
|
|
30
31
|
import orderDeliveryLoader from "./orderDeliveryLoader.js";
|
|
31
32
|
import orderPositionsLoader from "./orderPositionsLoader.js";
|
|
@@ -63,6 +64,7 @@ const loaders = (unchainedAPI) => {
|
|
|
63
64
|
paymentProviderLoader: paymentProviderLoader(unchainedAPI),
|
|
64
65
|
warehousingProviderLoader: warehousingProviderLoader(unchainedAPI),
|
|
65
66
|
orderLoader: orderLoader(unchainedAPI),
|
|
67
|
+
cartLoader: cartLoader(unchainedAPI),
|
|
66
68
|
orderPaymentLoader: orderPaymentLoader(unchainedAPI),
|
|
67
69
|
orderDeliveryLoader: orderDeliveryLoader(unchainedAPI),
|
|
68
70
|
orderPositionsLoader: orderPositionsLoader(unchainedAPI),
|
|
@@ -65,9 +65,8 @@ export async function createAuthContext(params, authConfig) {
|
|
|
65
65
|
const fingerprintCookie = getCookie(UNCHAINED_FINGERPRINT_COOKIE_NAME);
|
|
66
66
|
const verifyToken = createAuthHandler(authConfig);
|
|
67
67
|
const authResult = token ? await verifyToken(token) : {};
|
|
68
|
-
let fingerprintValid = true;
|
|
69
68
|
if (authResult.fingerprintHash && fingerprintCookie) {
|
|
70
|
-
fingerprintValid = verifyFingerprint(fingerprintCookie, authResult.fingerprintHash);
|
|
69
|
+
const fingerprintValid = verifyFingerprint(fingerprintCookie, authResult.fingerprintHash);
|
|
71
70
|
if (!fingerprintValid) {
|
|
72
71
|
logger.warn('Token sidejacking detected: fingerprint mismatch', {
|
|
73
72
|
userId: authResult.userId,
|
|
@@ -81,7 +80,6 @@ export async function createAuthContext(params, authConfig) {
|
|
|
81
80
|
logger.warn('Token sidejacking detected: fingerprint cookie missing', {
|
|
82
81
|
userId: authResult.userId,
|
|
83
82
|
});
|
|
84
|
-
fingerprintValid = false;
|
|
85
83
|
authResult.userId = undefined;
|
|
86
84
|
authResult.tokenVersion = undefined;
|
|
87
85
|
authResult.impersonatorId = undefined;
|
|
@@ -231,7 +231,7 @@ export default {
|
|
|
231
231
|
addCartProduct: acl(actions.updateCart)(addCartProduct),
|
|
232
232
|
addMultipleCartProducts: acl(actions.updateCart)(addMultipleCartProducts),
|
|
233
233
|
addCartDiscount: acl(actions.updateCart)(addCartDiscount),
|
|
234
|
-
addCartQuotation: acl(actions.
|
|
234
|
+
addCartQuotation: acl(actions.addCartQuotation)(addCartQuotation),
|
|
235
235
|
updateCart: acl(actions.updateCart)(updateCart),
|
|
236
236
|
emptyCart: acl(actions.updateCart)(emptyCart),
|
|
237
237
|
checkoutCart: acl(actions.checkoutCart)(checkoutCart),
|
|
@@ -301,7 +301,7 @@ export default {
|
|
|
301
301
|
verifyQuotation: acl(actions.manageQuotations)(verifyQuotation),
|
|
302
302
|
makeQuotationProposal: acl(actions.manageQuotations)(makeQuotationProposal),
|
|
303
303
|
bookmark: acl(actions.bookmarkProduct)(bookmark),
|
|
304
|
-
createBookmark: acl(actions.
|
|
304
|
+
createBookmark: acl(actions.createBookmark)(createBookmark),
|
|
305
305
|
removeBookmark: acl(actions.manageBookmarks)(removeBookmark),
|
|
306
306
|
addWork: acl(actions.manageWorker)(addWork),
|
|
307
307
|
allocateWork: acl(actions.manageWorker)(allocateWork),
|
|
@@ -309,6 +309,6 @@ export default {
|
|
|
309
309
|
removeWork: acl(actions.manageWorker)(removeWork),
|
|
310
310
|
processNextWork: acl(actions.manageWorker)(processNextWork),
|
|
311
311
|
signPaymentProviderForCredentialRegistration: acl(actions.registerPaymentCredentials)(signPaymentProviderForCredentialRegistration),
|
|
312
|
-
signPaymentProviderForCheckout: acl(actions.
|
|
312
|
+
signPaymentProviderForCheckout: acl(actions.updateOrderPayment)(signPaymentProviderForCheckout),
|
|
313
313
|
removeUserProductReviews: acl(actions.updateUser)(removeUserProductReviews),
|
|
314
314
|
};
|
|
@@ -49,9 +49,9 @@ export const User = {
|
|
|
49
49
|
return context.modules.bookmarks.findBookmarksByUserId(user._id);
|
|
50
50
|
},
|
|
51
51
|
async cart(user, params, context) {
|
|
52
|
-
const {
|
|
52
|
+
const { loaders, countryCode } = context;
|
|
53
53
|
await checkAction(context, viewUserOrders, [user, params]);
|
|
54
|
-
return
|
|
54
|
+
return loaders.cartLoader.load({
|
|
55
55
|
countryCode,
|
|
56
56
|
orderNumber: params.orderNumber,
|
|
57
57
|
userId: user._id,
|
package/lib/roles/index.js
CHANGED
|
@@ -66,6 +66,7 @@ const actions = [
|
|
|
66
66
|
'createEnrollment',
|
|
67
67
|
'updateEnrollment',
|
|
68
68
|
'updateCart',
|
|
69
|
+
'addCartQuotation',
|
|
69
70
|
'checkoutCart',
|
|
70
71
|
'updateOrder',
|
|
71
72
|
'updateOrderDelivery',
|
|
@@ -90,6 +91,7 @@ const actions = [
|
|
|
90
91
|
'manageQuotations',
|
|
91
92
|
'answerQuotation',
|
|
92
93
|
'bookmarkProduct',
|
|
94
|
+
'createBookmark',
|
|
93
95
|
'manageBookmarks',
|
|
94
96
|
'search',
|
|
95
97
|
'manageWorker',
|
package/lib/roles/loggedIn.js
CHANGED
|
@@ -116,6 +116,14 @@ export const loggedIn = (role, actions) => {
|
|
|
116
116
|
return true;
|
|
117
117
|
return bookmark.userId === userId;
|
|
118
118
|
};
|
|
119
|
+
const isOwnBookmarkUser = (obj, { userId: bookmarkUserId }, { userId }) => {
|
|
120
|
+
return !bookmarkUserId || bookmarkUserId === userId;
|
|
121
|
+
};
|
|
122
|
+
const isOwnedCartAndQuotation = async (obj, params, context) => {
|
|
123
|
+
if (!(await isOwnedOrderOrCart(obj, params, context)))
|
|
124
|
+
return false;
|
|
125
|
+
return isOwnedQuotation(obj, params, context);
|
|
126
|
+
};
|
|
119
127
|
const isOwnedPaymentCredential = async (obj, { paymentCredentialsId }, { modules, userId }) => {
|
|
120
128
|
const credentials = await modules.payment.paymentCredentials.findPaymentCredential({
|
|
121
129
|
paymentCredentialsId,
|
|
@@ -155,6 +163,7 @@ export const loggedIn = (role, actions) => {
|
|
|
155
163
|
role.allow(actions.updateOrderPayment, isOwnedOrderPayment);
|
|
156
164
|
role.allow(actions.checkoutCart, isOwnedOrderOrCart);
|
|
157
165
|
role.allow(actions.updateCart, isOwnedOrderOrCart);
|
|
166
|
+
role.allow(actions.addCartQuotation, isOwnedCartAndQuotation);
|
|
158
167
|
role.allow(actions.createCart, () => true);
|
|
159
168
|
role.allow(actions.viewEnrollment, isOwnedEnrollment);
|
|
160
169
|
role.allow(actions.updateEnrollment, isOwnedEnrollment);
|
|
@@ -164,6 +173,7 @@ export const loggedIn = (role, actions) => {
|
|
|
164
173
|
role.allow(actions.requestQuotation, () => true);
|
|
165
174
|
role.allow(actions.answerQuotation, isOwnedQuotation);
|
|
166
175
|
role.allow(actions.manageBookmarks, isOwnedBookmark);
|
|
176
|
+
role.allow(actions.createBookmark, isOwnBookmarkUser);
|
|
167
177
|
role.allow(actions.bookmarkProduct, () => true);
|
|
168
178
|
role.allow(actions.voteProductReview, () => true);
|
|
169
179
|
role.allow(actions.changePassword, () => true);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unchainedshop/api",
|
|
3
3
|
"description": "GraphQL API layer for the Unchained Engine with Express/Fastify adapters and MCP server",
|
|
4
|
-
"version": "4.8.
|
|
4
|
+
"version": "4.8.14",
|
|
5
5
|
"main": "lib/api-index.js",
|
|
6
6
|
"types": "lib/api-index.d.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -116,11 +116,11 @@
|
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
118
|
"dependencies": {
|
|
119
|
-
"@unchainedshop/core": "^4.
|
|
120
|
-
"@unchainedshop/events": "^4.
|
|
121
|
-
"@unchainedshop/logger": "^4.
|
|
122
|
-
"@unchainedshop/roles": "^4.
|
|
123
|
-
"@unchainedshop/utils": "^4.
|
|
119
|
+
"@unchainedshop/core": "^4.8.12",
|
|
120
|
+
"@unchainedshop/events": "^4.8.12",
|
|
121
|
+
"@unchainedshop/logger": "^4.8.12",
|
|
122
|
+
"@unchainedshop/roles": "^4.8.12",
|
|
123
|
+
"@unchainedshop/utils": "^4.8.12",
|
|
124
124
|
"dataloader": "^2.2.3",
|
|
125
125
|
"expiry-map": "^2.0.0",
|
|
126
126
|
"graphql-scalars": "^1.24.2",
|