@unchainedshop/api 4.8.23 → 4.8.24
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/adminUiPlugins.d.ts +24 -0
- package/lib/auth.d.ts +0 -8
- package/lib/auth.js +0 -20
- package/lib/loaders/assortmentLinksLoader.js +24 -22
- package/lib/loaders/assortmentProductLoader.js +7 -4
- package/lib/middleware/createAuthMiddleware.js +2 -39
- package/package.json +1 -1
- package/lib/adminUiTheme.d.ts +0 -25
- package/lib/adminUiTheme.js +0 -34
package/lib/adminUiPlugins.d.ts
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
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
|
+
}
|
|
1
25
|
export interface AdminUIPluginEntityConfig {
|
|
2
26
|
path: string;
|
|
3
27
|
label: string;
|
package/lib/auth.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ export interface AccessTokenPayload {
|
|
|
2
2
|
iss: string;
|
|
3
3
|
sub: string;
|
|
4
4
|
ver: number;
|
|
5
|
-
fgp?: string;
|
|
6
5
|
imp?: string;
|
|
7
6
|
jti?: string;
|
|
8
7
|
iat?: number;
|
|
@@ -16,14 +15,8 @@ export interface OIDCProviderConfig {
|
|
|
16
15
|
export interface AuthConfig {
|
|
17
16
|
oidcProviders?: OIDCProviderConfig[];
|
|
18
17
|
}
|
|
19
|
-
export declare function generateFingerprint(): {
|
|
20
|
-
raw: string;
|
|
21
|
-
hash: string;
|
|
22
|
-
};
|
|
23
|
-
export declare function verifyFingerprint(raw: string, hash: string): boolean;
|
|
24
18
|
export declare function signAccessToken(userId: string, tokenVersion: number, options?: {
|
|
25
19
|
impersonatorId?: string;
|
|
26
|
-
fingerprintHash?: string;
|
|
27
20
|
}): Promise<{
|
|
28
21
|
token: string;
|
|
29
22
|
expires: Date;
|
|
@@ -37,7 +30,6 @@ export interface AuthHandlerResult {
|
|
|
37
30
|
userId?: string;
|
|
38
31
|
tokenVersion?: number;
|
|
39
32
|
impersonatorId?: string;
|
|
40
|
-
fingerprintHash?: string;
|
|
41
33
|
accessToken?: string;
|
|
42
34
|
isApiKey?: boolean;
|
|
43
35
|
}
|
package/lib/auth.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createLogger } from '@unchainedshop/logger';
|
|
2
2
|
import * as jose from 'jose';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
4
3
|
const logger = createLogger('unchained:api:auth');
|
|
5
4
|
const { UNCHAINED_TOKEN_SECRET, UNCHAINED_TOKEN_EXPIRY_SECONDS = '3600', UNCHAINED_TOKEN_ISSUER = 'unchained-engine', } = process.env;
|
|
6
5
|
const MIN_SECRET_LENGTH = 32;
|
|
@@ -11,21 +10,6 @@ function validateSecretStrength(secret) {
|
|
|
11
10
|
`Current length: ${secret.length}. Generate a secure secret with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"`);
|
|
12
11
|
}
|
|
13
12
|
}
|
|
14
|
-
export function generateFingerprint() {
|
|
15
|
-
const raw = crypto.randomUUID() + crypto.randomUUID();
|
|
16
|
-
const hash = createHash('sha256').update(raw).digest('hex');
|
|
17
|
-
return { raw, hash };
|
|
18
|
-
}
|
|
19
|
-
export function verifyFingerprint(raw, hash) {
|
|
20
|
-
const computedHash = createHash('sha256').update(raw).digest('hex');
|
|
21
|
-
if (computedHash.length !== hash.length)
|
|
22
|
-
return false;
|
|
23
|
-
let result = 0;
|
|
24
|
-
for (let i = 0; i < computedHash.length; i++) {
|
|
25
|
-
result |= computedHash.charCodeAt(i) ^ hash.charCodeAt(i);
|
|
26
|
-
}
|
|
27
|
-
return result === 0;
|
|
28
|
-
}
|
|
29
13
|
export async function signAccessToken(userId, tokenVersion, options) {
|
|
30
14
|
if (!UNCHAINED_TOKEN_SECRET) {
|
|
31
15
|
throw new Error('UNCHAINED_TOKEN_SECRET environment variable is required');
|
|
@@ -38,9 +22,6 @@ export async function signAccessToken(userId, tokenVersion, options) {
|
|
|
38
22
|
ver: tokenVersion,
|
|
39
23
|
jti: crypto.randomUUID(),
|
|
40
24
|
};
|
|
41
|
-
if (options?.fingerprintHash) {
|
|
42
|
-
payload.fgp = options.fingerprintHash;
|
|
43
|
-
}
|
|
44
25
|
if (options?.impersonatorId) {
|
|
45
26
|
payload.imp = options.impersonatorId;
|
|
46
27
|
}
|
|
@@ -168,7 +149,6 @@ export function createAuthHandler(config) {
|
|
|
168
149
|
userId: localPayload.sub,
|
|
169
150
|
tokenVersion: localPayload.ver,
|
|
170
151
|
impersonatorId: localPayload.imp,
|
|
171
|
-
fingerprintHash: localPayload.fgp,
|
|
172
152
|
};
|
|
173
153
|
}
|
|
174
154
|
if (config?.oidcProviders?.length) {
|
|
@@ -1,38 +1,40 @@
|
|
|
1
1
|
import DataLoader from 'dataloader';
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
|
-
const parentAssortmentIds =
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const parentAssortmentIds = [
|
|
4
|
+
...new Set(queries.flatMap((q) => q.parentAssortmentId).filter(Boolean)),
|
|
5
|
+
];
|
|
6
|
+
const childAssortmentIds = [
|
|
7
|
+
...new Set(queries.flatMap((q) => q.childAssortmentId).filter(Boolean)),
|
|
8
|
+
];
|
|
9
|
+
const assortmentIds = [
|
|
10
|
+
...new Set(queries.flatMap((q) => q.assortmentId).filter(Boolean)),
|
|
11
|
+
];
|
|
6
12
|
const allLinks = await unchainedAPI.modules.assortments.links.findLinks({
|
|
7
|
-
|
|
13
|
+
...(parentAssortmentIds.length ? { parentAssortmentIds } : {}),
|
|
14
|
+
...(childAssortmentIds.length ? { childAssortmentIds } : {}),
|
|
15
|
+
...(assortmentIds.length ? { assortmentIds } : {}),
|
|
8
16
|
});
|
|
9
|
-
const parentAssortmentLinkMap =
|
|
10
|
-
const childAssortmentLinkMap =
|
|
17
|
+
const parentAssortmentLinkMap = new Map();
|
|
18
|
+
const childAssortmentLinkMap = new Map();
|
|
11
19
|
for (const link of allLinks) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!childAssortmentLinkMap[link.childAssortmentId]) {
|
|
19
|
-
childAssortmentLinkMap[link.childAssortmentId] = [link];
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
childAssortmentLinkMap[link.childAssortmentId].push(link);
|
|
23
|
-
}
|
|
20
|
+
const parentLinks = parentAssortmentLinkMap.get(link.parentAssortmentId) || [];
|
|
21
|
+
parentLinks.push(link);
|
|
22
|
+
parentAssortmentLinkMap.set(link.parentAssortmentId, parentLinks);
|
|
23
|
+
const childLinks = childAssortmentLinkMap.get(link.childAssortmentId) || [];
|
|
24
|
+
childLinks.push(link);
|
|
25
|
+
childAssortmentLinkMap.set(link.childAssortmentId, childLinks);
|
|
24
26
|
}
|
|
25
27
|
return queries.map((q) => {
|
|
26
28
|
if (q.parentAssortmentId) {
|
|
27
|
-
return parentAssortmentLinkMap
|
|
29
|
+
return parentAssortmentLinkMap.get(q.parentAssortmentId) || [];
|
|
28
30
|
}
|
|
29
31
|
else if (q.childAssortmentId) {
|
|
30
|
-
return childAssortmentLinkMap
|
|
32
|
+
return childAssortmentLinkMap.get(q.childAssortmentId) || [];
|
|
31
33
|
}
|
|
32
34
|
if (q.assortmentId) {
|
|
33
35
|
return [
|
|
34
|
-
...(parentAssortmentLinkMap
|
|
35
|
-
...(childAssortmentLinkMap
|
|
36
|
+
...(parentAssortmentLinkMap.get(q.assortmentId) || []),
|
|
37
|
+
...(childAssortmentLinkMap.get(q.assortmentId) || []),
|
|
36
38
|
];
|
|
37
39
|
}
|
|
38
40
|
return [];
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import DataLoader from 'dataloader';
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
3
|
const assortmentIds = [...new Set(queries.map((q) => q.assortmentId).filter(Boolean))];
|
|
4
|
+
const productIds = [...new Set(queries.map((q) => q.productId).filter(Boolean))];
|
|
4
5
|
const assortmentProducts = await unchainedAPI.modules.assortments.products.findAssortmentProducts({
|
|
5
6
|
assortmentIds,
|
|
7
|
+
productIds,
|
|
6
8
|
});
|
|
7
|
-
const assortmentProductMap =
|
|
9
|
+
const assortmentProductMap = new Map();
|
|
8
10
|
for (const assortmentProduct of assortmentProducts) {
|
|
9
|
-
assortmentProductMap
|
|
10
|
-
|
|
11
|
+
const productsById = assortmentProductMap.get(assortmentProduct.assortmentId) || new Map();
|
|
12
|
+
productsById.set(assortmentProduct.productId, assortmentProduct);
|
|
13
|
+
assortmentProductMap.set(assortmentProduct.assortmentId, productsById);
|
|
11
14
|
}
|
|
12
|
-
return queries.map((q) => assortmentProductMap
|
|
15
|
+
return queries.map((q) => assortmentProductMap.get(q.assortmentId)?.get(q.productId));
|
|
13
16
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { emit } from '@unchainedshop/events';
|
|
2
|
-
import { signAccessToken, createAuthHandler
|
|
2
|
+
import { signAccessToken, createAuthHandler } from "../auth.js";
|
|
3
3
|
import { API_EVENTS } from "../events.js";
|
|
4
4
|
import { createLogger } from '@unchainedshop/logger';
|
|
5
5
|
const logger = createLogger('unchained:api:auth-middleware');
|
|
6
|
-
const { UNCHAINED_COOKIE_NAME = 'unchained_token', UNCHAINED_COOKIE_PATH = '/', UNCHAINED_COOKIE_DOMAIN, UNCHAINED_COOKIE_SAMESITE = 'lax', UNCHAINED_COOKIE_INSECURE,
|
|
6
|
+
const { UNCHAINED_COOKIE_NAME = 'unchained_token', UNCHAINED_COOKIE_PATH = '/', UNCHAINED_COOKIE_DOMAIN, UNCHAINED_COOKIE_SAMESITE = 'lax', UNCHAINED_COOKIE_INSECURE, UNCHAINED_TOKEN_EXPIRY_SECONDS = '3600', } = process.env;
|
|
7
7
|
function getTokenCookieOptions(expires) {
|
|
8
8
|
const secure = !UNCHAINED_COOKIE_INSECURE;
|
|
9
9
|
const sameSite = {
|
|
@@ -30,19 +30,6 @@ function getTokenCookieOptions(expires) {
|
|
|
30
30
|
expires,
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
function getFingerprintCookieOptions(expires) {
|
|
34
|
-
const secure = !UNCHAINED_COOKIE_INSECURE;
|
|
35
|
-
const expirySeconds = parseInt(UNCHAINED_TOKEN_EXPIRY_SECONDS, 10);
|
|
36
|
-
return {
|
|
37
|
-
domain: UNCHAINED_COOKIE_DOMAIN,
|
|
38
|
-
path: UNCHAINED_COOKIE_PATH,
|
|
39
|
-
secure,
|
|
40
|
-
httpOnly: true,
|
|
41
|
-
sameSite: 'strict',
|
|
42
|
-
maxAge: expires ? undefined : expirySeconds * 1000,
|
|
43
|
-
expires,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
33
|
function extractBearerToken(authHeader) {
|
|
47
34
|
if (!authHeader)
|
|
48
35
|
return undefined;
|
|
@@ -62,38 +49,15 @@ export async function createAuthContext(params, authConfig) {
|
|
|
62
49
|
const headerToken = extractBearerToken(authHeader);
|
|
63
50
|
const cookieToken = getCookie(UNCHAINED_COOKIE_NAME);
|
|
64
51
|
const token = headerToken || cookieToken;
|
|
65
|
-
const fingerprintCookie = getCookie(UNCHAINED_FINGERPRINT_COOKIE_NAME);
|
|
66
52
|
const verifyToken = createAuthHandler(authConfig);
|
|
67
53
|
const authResult = token ? await verifyToken(token) : {};
|
|
68
|
-
if (authResult.fingerprintHash && fingerprintCookie) {
|
|
69
|
-
const fingerprintValid = verifyFingerprint(fingerprintCookie, authResult.fingerprintHash);
|
|
70
|
-
if (!fingerprintValid) {
|
|
71
|
-
logger.warn('Token sidejacking detected: fingerprint mismatch', {
|
|
72
|
-
userId: authResult.userId,
|
|
73
|
-
});
|
|
74
|
-
authResult.userId = undefined;
|
|
75
|
-
authResult.tokenVersion = undefined;
|
|
76
|
-
authResult.impersonatorId = undefined;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
else if (authResult.fingerprintHash && !fingerprintCookie) {
|
|
80
|
-
logger.warn('Token sidejacking detected: fingerprint cookie missing', {
|
|
81
|
-
userId: authResult.userId,
|
|
82
|
-
});
|
|
83
|
-
authResult.userId = undefined;
|
|
84
|
-
authResult.tokenVersion = undefined;
|
|
85
|
-
authResult.impersonatorId = undefined;
|
|
86
|
-
}
|
|
87
54
|
const login = async (user, options = {}) => {
|
|
88
55
|
const { impersonator } = options;
|
|
89
56
|
const tokenVersion = user.tokenVersion ?? 1;
|
|
90
|
-
const { raw: fingerprintRaw, hash: fingerprintHash } = generateFingerprint();
|
|
91
57
|
const { token: newToken, expires } = await signAccessToken(user._id, tokenVersion, {
|
|
92
58
|
impersonatorId: impersonator?._id,
|
|
93
|
-
fingerprintHash,
|
|
94
59
|
});
|
|
95
60
|
setCookie(UNCHAINED_COOKIE_NAME, newToken, getTokenCookieOptions(expires));
|
|
96
|
-
setCookie(UNCHAINED_FINGERPRINT_COOKIE_NAME, fingerprintRaw, getFingerprintCookieOptions(expires));
|
|
97
61
|
const tokenObject = {
|
|
98
62
|
_id: crypto.randomUUID(),
|
|
99
63
|
userId: user._id,
|
|
@@ -105,7 +69,6 @@ export async function createAuthContext(params, authConfig) {
|
|
|
105
69
|
};
|
|
106
70
|
const logout = async () => {
|
|
107
71
|
clearCookie(UNCHAINED_COOKIE_NAME, getTokenCookieOptions());
|
|
108
|
-
clearCookie(UNCHAINED_FINGERPRINT_COOKIE_NAME, getFingerprintCookieOptions());
|
|
109
72
|
if (authResult.userId) {
|
|
110
73
|
const tokenObject = {
|
|
111
74
|
_id: crypto.randomUUID(),
|
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.24",
|
|
5
5
|
"main": "lib/api-index.js",
|
|
6
6
|
"types": "lib/api-index.d.ts",
|
|
7
7
|
"type": "module",
|
package/lib/adminUiTheme.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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;
|
package/lib/adminUiTheme.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
};
|