@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.
@@ -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 = queries.flatMap((q) => q.parentAssortmentId).filter(Boolean);
4
- const childAssortmentIds = queries.flatMap((q) => q.childAssortmentId).filter(Boolean);
5
- const assortmentIds = queries.flatMap((q) => q.assortmentId).filter(Boolean);
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
- assortmentIds: [...new Set([...parentAssortmentIds, ...childAssortmentIds, ...assortmentIds])],
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
- if (!parentAssortmentLinkMap[link.parentAssortmentId]) {
13
- parentAssortmentLinkMap[link.parentAssortmentId] = [link];
14
- }
15
- else {
16
- parentAssortmentLinkMap[link.parentAssortmentId].push(link);
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[q.parentAssortmentId] || [];
29
+ return parentAssortmentLinkMap.get(q.parentAssortmentId) || [];
28
30
  }
29
31
  else if (q.childAssortmentId) {
30
- return childAssortmentLinkMap[q.childAssortmentId] || [];
32
+ return childAssortmentLinkMap.get(q.childAssortmentId) || [];
31
33
  }
32
34
  if (q.assortmentId) {
33
35
  return [
34
- ...(parentAssortmentLinkMap[q.assortmentId] || []),
35
- ...(childAssortmentLinkMap[q.assortmentId] || []),
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[assortmentProduct.assortmentId + assortmentProduct.productId] =
10
- assortmentProduct;
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[q.assortmentId + q.productId]);
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, generateFingerprint, verifyFingerprint, } from "../auth.js";
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, UNCHAINED_FINGERPRINT_COOKIE_NAME = '__Secure-fgp', UNCHAINED_TOKEN_EXPIRY_SECONDS = '3600', } = process.env;
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.23",
4
+ "version": "4.8.24",
5
5
  "main": "lib/api-index.js",
6
6
  "types": "lib/api-index.d.ts",
7
7
  "type": "module",
@@ -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;
@@ -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
- };