@unchainedshop/api 5.0.0-alpha.3 → 5.0.0-alpha.5

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.
Files changed (91) hide show
  1. package/README.md +2 -2
  2. package/lib/acl.js +4 -0
  3. package/lib/adminUiPlugins.d.ts +36 -1
  4. package/lib/adminUiPlugins.js +188 -26
  5. package/lib/auth.d.ts +0 -8
  6. package/lib/auth.js +0 -20
  7. package/lib/errors.d.ts +2 -0
  8. package/lib/errors.js +2 -0
  9. package/lib/events.d.ts +1 -0
  10. package/lib/events.js +1 -0
  11. package/lib/express/chatHandler.js +12 -1
  12. package/lib/express/index.d.ts +2 -4
  13. package/lib/express/index.js +63 -24
  14. package/lib/fastify/chatHandler.js +10 -1
  15. package/lib/fastify/index.d.ts +4 -4
  16. package/lib/fastify/index.js +83 -30
  17. package/lib/loaders/assortmentLinksLoader.js +24 -22
  18. package/lib/loaders/assortmentMediaTextLoader.js +1 -0
  19. package/lib/loaders/assortmentProductLoader.js +7 -4
  20. package/lib/loaders/assortmentTextLoader.js +1 -0
  21. package/lib/loaders/filterTextLoader.js +1 -0
  22. package/lib/loaders/productMediaTextLoader.js +1 -0
  23. package/lib/loaders/productTextLoader.js +1 -0
  24. package/lib/loaders/productVariationTextLoader.js +1 -0
  25. package/lib/mcp/tools/assortment/handlers/getAssortmentFilters.js +1 -1
  26. package/lib/mcp/tools/assortment/handlers/getAssortmentProducts.js +1 -1
  27. package/lib/mcp/tools/system/handlers/allocateWork.js +2 -2
  28. package/lib/mcp/tools/users/handlers/getUserPaymentCredentials.js +1 -0
  29. package/lib/mcp/tools/users/index.js +1 -1
  30. package/lib/mcp/utils/getNormalizedAssortmentDetails.js +2 -2
  31. package/lib/mcp/utils/getNormalizedUserDetails.js +1 -0
  32. package/lib/middleware/createAuthMiddleware.js +4 -39
  33. package/lib/resolvers/mutations/accounts/loginWithPassword.js +16 -2
  34. package/lib/resolvers/mutations/accounts/logoutAllSessions.d.ts +3 -1
  35. package/lib/resolvers/mutations/accounts/logoutAllSessions.js +10 -4
  36. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.d.ts +10 -0
  37. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.js +18 -0
  38. package/lib/resolvers/mutations/bulk/bulkOperation.d.ts +12 -0
  39. package/lib/resolvers/mutations/bulk/bulkOperation.js +14 -0
  40. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.d.ts +9 -0
  41. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.js +7 -0
  42. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.d.ts +9 -0
  43. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.js +9 -0
  44. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.d.ts +9 -0
  45. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.js +9 -0
  46. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.d.ts +9 -0
  47. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.js +7 -0
  48. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.d.ts +10 -0
  49. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.js +9 -0
  50. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.d.ts +10 -0
  51. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.js +10 -0
  52. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.d.ts +10 -0
  53. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.js +17 -0
  54. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.d.ts +10 -0
  55. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.js +14 -0
  56. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.d.ts +11 -0
  57. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.js +10 -0
  58. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.d.ts +11 -0
  59. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.js +7 -0
  60. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.d.ts +11 -0
  61. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.js +7 -0
  62. package/lib/resolvers/mutations/index.d.ts +12 -0
  63. package/lib/resolvers/mutations/index.js +25 -1
  64. package/lib/resolvers/mutations/orders/checkoutCart.js +1 -1
  65. package/lib/resolvers/mutations/users/removeUser.js +9 -2
  66. package/lib/resolvers/mutations/users/setRoles.js +9 -2
  67. package/lib/resolvers/mutations/worker/allocateWork.d.ts +1 -1
  68. package/lib/resolvers/mutations/worker/allocateWork.js +4 -3
  69. package/lib/resolvers/queries/index.d.ts +1 -0
  70. package/lib/resolvers/queries/index.js +2 -0
  71. package/lib/resolvers/queries/search/globalSearch.d.ts +36 -0
  72. package/lib/resolvers/queries/search/globalSearch.js +200 -0
  73. package/lib/resolvers/type/assortment/assortment-types.js +2 -2
  74. package/lib/resolvers/type/global-search-result-types.d.ts +3 -0
  75. package/lib/resolvers/type/global-search-result-types.js +15 -0
  76. package/lib/resolvers/type/index.d.ts +3 -0
  77. package/lib/resolvers/type/index.js +2 -0
  78. package/lib/resolvers/type/user-types.d.ts +1 -0
  79. package/lib/resolvers/type/user-types.js +36 -4
  80. package/lib/roles/all.js +12 -0
  81. package/lib/roles/index.js +12 -0
  82. package/lib/roles/loggedIn.js +1 -1
  83. package/lib/schema/inputTypes.js +5 -0
  84. package/lib/schema/mutation.js +74 -1
  85. package/lib/schema/query.js +15 -0
  86. package/lib/schema/types/common.js +7 -0
  87. package/lib/schema/types/search.js +36 -0
  88. package/lib/schema/types/user.js +4 -0
  89. package/package.json +31 -26
  90. package/lib/adminUiTheme.d.ts +0 -25
  91. package/lib/adminUiTheme.js +0 -34
package/README.md CHANGED
@@ -168,8 +168,8 @@ The API layer implements comprehensive security controls.
168
168
  |----------|---------|---------|
169
169
  | `UNCHAINED_TOKEN_SECRET` | Session encryption (min 32 chars) | Required |
170
170
  | `UNCHAINED_COOKIE_NAME` | Cookie name | `unchained_token` |
171
- | `UNCHAINED_COOKIE_SAMESITE` | SameSite attribute | `none` |
172
- | `UNCHAINED_COOKIE_INSECURE` | Disable secure flag | `false` |
171
+ | `UNCHAINED_COOKIE_SAMESITE` | SameSite attribute | `lax` |
172
+ | `UNCHAINED_COOKIE_INSECURE` | Disable secure flag (development only) | `false` |
173
173
 
174
174
  Cookies are `httpOnly` and `secure` by default.
175
175
 
package/lib/acl.js CHANGED
@@ -9,7 +9,11 @@ const SENSITIVE_ACTION_PREFIXES = [
9
9
  'forgot',
10
10
  'impersonate',
11
11
  'updateUser',
12
+ 'removeUser',
12
13
  'createUser',
14
+ 'bulkSetUserRoles',
15
+ 'bulkRemoveUsers',
16
+ 'bulkUpdateUserTags',
13
17
  ];
14
18
  const defaultOptions = {
15
19
  showKey: true,
@@ -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;
@@ -61,13 +85,24 @@ interface StaticAsset {
61
85
  export interface PreparedPluginAssets {
62
86
  routes: Map<string, StaticAsset>;
63
87
  validPlugins: AdminUIPluginConfig[];
88
+ importMapTag: string | null;
89
+ importMapJSON: string | null;
90
+ }
91
+ export interface PreparedAdminUIHTML {
92
+ htmlByRoute: Map<string, string>;
93
+ rootHtml: string;
94
+ extHtml: string | null;
64
95
  }
65
96
  export declare const resolveAdminUIPath: () => string | null;
66
- export declare const parseBundleExports: (bundle: string) => Set<string> | null;
97
+ export declare const prepareAdminUIHTML: (adminUIPath: string) => PreparedAdminUIHTML;
98
+ export declare const resolveAdminUIHTML: (prepared: PreparedAdminUIHTML, urlPath: string, prefix?: string) => string;
99
+ export declare const injectAdminUIImportMap: (prepared: PreparedAdminUIHTML, urlPath: string, importMapTag: string, prefix?: string) => string;
100
+ export declare const parseEsmBundleExports: (bundle: string) => Set<string> | null;
67
101
  export declare function preparePluginAssets(plugins: AdminUIPluginConfig[], log: {
68
102
  info: (...args: any[]) => void;
69
103
  warn: (...args: any[]) => void;
70
104
  }, options?: {
71
105
  devMode?: boolean;
72
106
  }): PreparedPluginAssets;
107
+ export declare function buildImportMapTag(importMapJSON: string, nonce?: string): string;
73
108
  export {};
@@ -1,8 +1,23 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { readFileSync, statSync } from 'node:fs';
3
- import { resolve } from 'node:path';
2
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { join, resolve, sep } from 'node:path';
4
5
  const PLUGIN_NAME_RE = /^[a-z0-9]([a-z0-9_-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9_-]*[a-z0-9])?)*$/i;
5
6
  const IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';
7
+ const requireFromAPI = createRequire(import.meta.url);
8
+ const loadPluginRuntime = (log) => {
9
+ try {
10
+ const runtime = requireFromAPI('@unchainedshop/admin-ui/plugin-runtime');
11
+ if (!runtime.SHARED_DEP_SHIMS || !runtime.SDK_MODULE_FILES) {
12
+ throw new Error('runtime module does not expose the expected module graph');
13
+ }
14
+ return runtime;
15
+ }
16
+ catch (err) {
17
+ log.warn(`admin-ui plugin runtime could not be loaded; shared plugin modules will be unavailable: ${err.message}`);
18
+ return null;
19
+ }
20
+ };
6
21
  export const resolveAdminUIPath = () => {
7
22
  try {
8
23
  const staticURL = import.meta.resolve('@unchainedshop/admin-ui');
@@ -12,17 +27,82 @@ export const resolveAdminUIPath = () => {
12
27
  return null;
13
28
  }
14
29
  };
30
+ const trimTrailingSlashes = (value) => {
31
+ let end = value.length;
32
+ while (end > 0 && value[end - 1] === '/')
33
+ end -= 1;
34
+ return value.slice(0, end);
35
+ };
36
+ const normalizeAdminUIRoute = (urlPath, prefix = '/') => {
37
+ let pathOnly = urlPath.split('?')[0];
38
+ const normalizedPrefix = `/${trimTrailingSlashes(prefix.replace(/^\/+/, ''))}`;
39
+ if (normalizedPrefix !== '/' &&
40
+ (pathOnly === normalizedPrefix || pathOnly.startsWith(`${normalizedPrefix}/`))) {
41
+ pathOnly = pathOnly.slice(normalizedPrefix.length) || '/';
42
+ }
43
+ const withLeadingSlash = pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`;
44
+ return trimTrailingSlashes(withLeadingSlash) || '/';
45
+ };
46
+ export const prepareAdminUIHTML = (adminUIPath) => {
47
+ const htmlByRoute = new Map();
48
+ let extHtml = null;
49
+ for (const entry of readdirSync(adminUIPath, { recursive: true })) {
50
+ const relativePath = String(entry).split(sep).join('/');
51
+ if (relativePath !== 'index.html' && !relativePath.endsWith('/index.html'))
52
+ continue;
53
+ const route = relativePath === 'index.html' ? '/' : `/${relativePath.slice(0, -'/index.html'.length)}`;
54
+ const html = readFileSync(join(adminUIPath, relativePath), 'utf-8');
55
+ if (route === '/ext/[[...slug]]') {
56
+ extHtml = html;
57
+ }
58
+ else if (!route.includes('[')) {
59
+ htmlByRoute.set(route, html);
60
+ }
61
+ }
62
+ const rootHtml = htmlByRoute.get('/');
63
+ if (!rootHtml) {
64
+ throw new Error(`admin-ui root index.html not found in ${adminUIPath}`);
65
+ }
66
+ return { htmlByRoute, rootHtml, extHtml };
67
+ };
68
+ export const resolveAdminUIHTML = (prepared, urlPath, prefix = '/') => {
69
+ const route = normalizeAdminUIRoute(urlPath, prefix);
70
+ const exactHtml = prepared.htmlByRoute.get(route);
71
+ if (exactHtml)
72
+ return exactHtml;
73
+ if (prepared.extHtml && (route === '/ext' || route.startsWith('/ext/'))) {
74
+ return prepared.extHtml;
75
+ }
76
+ return prepared.rootHtml;
77
+ };
78
+ export const injectAdminUIImportMap = (prepared, urlPath, importMapTag, prefix = '/') => resolveAdminUIHTML(prepared, urlPath, prefix).replace('</head>', `${importMapTag}</head>`);
15
79
  const contentHash = (content) => createHash('sha256').update(content).digest('hex').slice(0, 8);
16
- export const parseBundleExports = (bundle) => {
17
- const returnMatch = bundle.match(/return __toCommonJS\(([\w$]+)\);/);
18
- if (!returnMatch)
19
- return null;
20
- const blockMatch = bundle.match(new RegExp(`__export\\(${returnMatch[1].replace(/\$/g, '\\$')},\\s*\\{([\\s\\S]*?)\\}\\);`));
21
- if (!blockMatch)
22
- return null;
80
+ const listSDKDistFiles = (distPath) => {
81
+ try {
82
+ if (!statSync(distPath).isDirectory())
83
+ return [];
84
+ return readdirSync(distPath, { recursive: true })
85
+ .map((file) => String(file).split(sep).join('/'))
86
+ .filter((file) => file.endsWith('.js'));
87
+ }
88
+ catch {
89
+ return [];
90
+ }
91
+ };
92
+ export const parseEsmBundleExports = (bundle) => {
23
93
  const names = new Set();
24
- for (const m of blockMatch[1].matchAll(/(?:^|,)\s*(?:"([^"]+)"|([\w$]+)):\s*\(\)\s*=>/g)) {
25
- names.add(m[1] ?? m[2]);
94
+ for (const clause of bundle.matchAll(/(?:^|[;\n])\s*export\s*\{([^}]*)\}/g)) {
95
+ for (const entry of clause[1].split(',')) {
96
+ const parts = entry.trim().split(/\s+as\s+/);
97
+ const exported = (parts[1] ?? parts[0]).trim().replace(/^["']|["']$/g, '');
98
+ if (exported)
99
+ names.add(exported);
100
+ }
101
+ }
102
+ if (/(?:^|[;\n])\s*export\s+default\b/.test(bundle))
103
+ names.add('default');
104
+ for (const decl of bundle.matchAll(/(?:^|[;\n])\s*export\s+(?:async\s+)?(?:const|let|var|function|class)\s+([\w$]+)/g)) {
105
+ names.add(decl[1]);
26
106
  }
27
107
  return names.size > 0 ? names : null;
28
108
  };
@@ -48,24 +128,41 @@ const collectReferencedComponents = (plugin) => {
48
128
  }
49
129
  return [...names];
50
130
  };
131
+ const parseBundleSdkVersion = (bundle) => {
132
+ const match = bundle.match(/^\/\* unchained admin-ui plugin: .+? \(esm, sdk ([^)]+)\) \*\//);
133
+ return match?.[1] ?? null;
134
+ };
135
+ const readHostSdkVersion = () => {
136
+ const adminUIPath = resolveAdminUIPath();
137
+ if (!adminUIPath)
138
+ return null;
139
+ try {
140
+ const pkg = JSON.parse(readFileSync(join(adminUIPath, '..', 'package.json'), 'utf-8'));
141
+ return typeof pkg.version === 'string' ? pkg.version : null;
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ };
51
147
  export function preparePluginAssets(plugins, log, options = {}) {
52
148
  const { devMode = false } = options;
53
149
  const routes = new Map();
54
150
  const devCacheControl = 'no-cache, no-store, must-revalidate';
55
- const validPlugins = plugins.filter((p) => {
151
+ const namedPlugins = plugins.filter((p) => {
56
152
  if (!PLUGIN_NAME_RE.test(p.name)) {
57
153
  log.warn(`Skipping admin-ui plugin with invalid name: "${p.name}"`);
58
154
  return false;
59
155
  }
60
156
  return true;
61
157
  });
62
- const seenNames = new Set();
63
- for (const plugin of validPlugins) {
64
- if (seenNames.has(plugin.name)) {
158
+ const validPlugins = namedPlugins.filter((plugin, index) => {
159
+ const lastIndex = namedPlugins.findLastIndex((p) => p.name === plugin.name);
160
+ if (index !== lastIndex) {
65
161
  log.warn(`Duplicate admin-ui plugin name "${plugin.name}": later entries override earlier ones`);
162
+ return false;
66
163
  }
67
- seenNames.add(plugin.name);
68
- }
164
+ return true;
165
+ });
69
166
  if (validPlugins.length > 0) {
70
167
  const pluginList = validPlugins
71
168
  .map((p) => `${p.name}${p.version ? `@${p.version}` : ''}`)
@@ -86,13 +183,19 @@ export function preparePluginAssets(plugins, log, options = {}) {
86
183
  }
87
184
  }
88
185
  const pluginsWithBundles = validPlugins.filter((p) => pluginBundles.has(p.name));
186
+ const hostSdkVersion = pluginsWithBundles.length > 0 ? readHostSdkVersion() : null;
89
187
  for (const plugin of pluginsWithBundles) {
90
- const exportNames = parseBundleExports(pluginBundles.get(plugin.name).content);
91
- if (!exportNames)
92
- continue;
93
- const missing = collectReferencedComponents(plugin).filter((name) => !exportNames.has(name));
94
- if (missing.length > 0) {
95
- log.warn(`admin-ui plugin "${plugin.name}" references component(s) not exported by its bundle: ${missing.join(', ')}. Exported: ${[...exportNames].join(', ')}`);
188
+ const bundle = pluginBundles.get(plugin.name).content;
189
+ const exportNames = parseEsmBundleExports(bundle);
190
+ if (exportNames) {
191
+ const missing = collectReferencedComponents(plugin).filter((name) => !exportNames.has(name));
192
+ if (missing.length > 0) {
193
+ log.warn(`admin-ui plugin "${plugin.name}" references component(s) not exported by its bundle: ${missing.join(', ')}. Exported: ${[...exportNames].join(', ')}`);
194
+ }
195
+ }
196
+ const bundleSdkVersion = parseBundleSdkVersion(bundle);
197
+ if (hostSdkVersion && bundleSdkVersion && bundleSdkVersion !== hostSdkVersion) {
198
+ log.warn(`admin-ui plugin "${plugin.name}" was built against SDK ${bundleSdkVersion} but the running admin-ui is ${hostSdkVersion}. It may still work, but rebuild it against the current @unchainedshop/admin-ui to be safe.`);
96
199
  }
97
200
  }
98
201
  const bundleMtimes = new Map();
@@ -147,7 +250,7 @@ export function preparePluginAssets(plugins, log, options = {}) {
147
250
  if (devMode) {
148
251
  routes.set(`/admin-plugins/${plugin.name}.js`, {
149
252
  content: () => readFileSync(bundlePath, 'utf-8'),
150
- contentType: 'application/javascript',
253
+ contentType: 'text/javascript',
151
254
  cacheControl: devCacheControl,
152
255
  });
153
256
  }
@@ -155,10 +258,69 @@ export function preparePluginAssets(plugins, log, options = {}) {
155
258
  const { content } = pluginBundles.get(plugin.name);
156
259
  routes.set(`/admin-plugins/${plugin.name}.js`, {
157
260
  content,
158
- contentType: 'application/javascript',
261
+ contentType: 'text/javascript',
159
262
  cacheControl: IMMUTABLE_CACHE,
160
263
  });
161
264
  }
162
265
  }
163
- return { routes, validPlugins };
266
+ let importMapTag = null;
267
+ let importMapContent = null;
268
+ if (pluginBundles.size > 0) {
269
+ const runtime = loadPluginRuntime(log);
270
+ const adminUIPath = resolveAdminUIPath();
271
+ const distPath = adminUIPath ? join(adminUIPath, '..', 'dist') : null;
272
+ const sdkFiles = distPath ? listSDKDistFiles(distPath) : [];
273
+ if (runtime && distPath && sdkFiles.length > 0) {
274
+ const sdkFileHashes = new Map();
275
+ for (const file of sdkFiles) {
276
+ const filePath = join(distPath, file);
277
+ if (devMode) {
278
+ routes.set(`/admin-ui-sdk/${file}`, {
279
+ content: () => readFileSync(filePath, 'utf-8'),
280
+ contentType: 'text/javascript',
281
+ cacheControl: devCacheControl,
282
+ });
283
+ }
284
+ else {
285
+ const content = readFileSync(filePath, 'utf-8');
286
+ sdkFileHashes.set(file, contentHash(content));
287
+ routes.set(`/admin-ui-sdk/${file}`, {
288
+ content,
289
+ contentType: 'text/javascript',
290
+ cacheControl: IMMUTABLE_CACHE,
291
+ });
292
+ }
293
+ }
294
+ const imports = {};
295
+ for (const [specifier, file] of [
296
+ ...Object.entries(runtime.SDK_MODULE_FILES),
297
+ ...Object.entries(runtime.SHARED_DEP_SHIMS),
298
+ ]) {
299
+ if (!sdkFiles.includes(file)) {
300
+ log.warn(`admin-ui plugin runtime: expected SDK file "${file}" for "${specifier}" not found in ${distPath}`);
301
+ continue;
302
+ }
303
+ const version = sdkFileHashes.get(file);
304
+ imports[specifier] = `/admin-ui-sdk/${file}${version ? `?v=${version}` : ''}`;
305
+ }
306
+ const importMapJSON = JSON.stringify({ imports });
307
+ const importMapHash = contentHash(importMapJSON);
308
+ routes.set('/admin-ui-importmap.json', {
309
+ content: importMapJSON,
310
+ contentType: 'application/json',
311
+ cacheControl: devMode ? devCacheControl : 'public, max-age=0, must-revalidate',
312
+ etag: `"${importMapHash}"`,
313
+ });
314
+ importMapContent = importMapJSON;
315
+ importMapTag = `<script type="importmap" data-unchained-admin-ui>${importMapJSON}</script>`;
316
+ }
317
+ else if (runtime) {
318
+ log.warn('admin-ui plugin runtime: SDK dist files not found; plugins depending on shared modules will fail to load');
319
+ }
320
+ }
321
+ return { routes, validPlugins, importMapTag, importMapJSON: importMapContent };
322
+ }
323
+ export function buildImportMapTag(importMapJSON, nonce) {
324
+ const nonceAttr = nonce ? ` nonce="${nonce}"` : '';
325
+ return `<script type="importmap" data-unchained-admin-ui${nonceAttr}>${importMapJSON}</script>`;
164
326
  }
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) {
package/lib/errors.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare const CurrencyNotFoundError: any;
19
19
  export declare const DeliverProviderNotFoundError: any;
20
20
  export declare const LanguageNotFoundError: any;
21
21
  export declare const UserNotFoundError: any;
22
+ export declare const LastAdminError: any;
22
23
  export declare const UserWebAuthnCredentialsNotFoundError: any;
23
24
  export declare const WebAuthnDisabledError: any;
24
25
  export declare const UserWeb3AddressNotFoundError: any;
@@ -56,6 +57,7 @@ export declare const EnrollmentWrongStatusError: any;
56
57
  export declare const EnrollmentNotFoundError: any;
57
58
  export declare const WarehousingProviderNotFoundError: any;
58
59
  export declare const InvalidIdError: any;
60
+ export declare const BulkOperationTooLargeError: any;
59
61
  export declare const ProviderConfigurationInvalid: any;
60
62
  export declare const TokenWrongStatusError: any;
61
63
  export declare const TokenNotFoundError: any;
package/lib/errors.js CHANGED
@@ -29,6 +29,7 @@ export const CurrencyNotFoundError = createError('CurrencyNotFoundError', 'Curre
29
29
  export const DeliverProviderNotFoundError = createError('DeliverProviderNotFoundError', 'Delivery provider not found');
30
30
  export const LanguageNotFoundError = createError('LanguageNotFoundError', 'Language not found');
31
31
  export const UserNotFoundError = createError('UserNotFoundError', 'User not found');
32
+ export const LastAdminError = createError('LastAdminError', 'At least one active administrator is required. Assign another administrator before removing this account or revoking its admin role.');
32
33
  export const UserWebAuthnCredentialsNotFoundError = createError('UserWebAuthnCredentialsNotFoundError', 'User WebAuth Credentials not found');
33
34
  export const WebAuthnDisabledError = createError('WebAuthnDisabledError', 'WebAuthn disabled');
34
35
  export const UserWeb3AddressNotFoundError = createError('UserWeb3AddressNotFoundError', 'Web3 Account not found for User');
@@ -66,6 +67,7 @@ export const EnrollmentWrongStatusError = createError('EnrollmentWrongStatusErro
66
67
  export const EnrollmentNotFoundError = createError('EnrollmentNotFoundError', 'Enrollment not found');
67
68
  export const WarehousingProviderNotFoundError = createError('WarehousingProviderNotFoundError', 'Warehousing provider not found');
68
69
  export const InvalidIdError = createError('InvalidIdError', 'Invalid ID provided');
70
+ export const BulkOperationTooLargeError = createError('BulkOperationTooLargeError', 'Bulk operation exceeds the maximum number of entities');
69
71
  export const ProviderConfigurationInvalid = createError('ProviderConfigurationInvalid', 'Provider Configuration invalid (check if the Adapter Key exists)');
70
72
  export const TokenWrongStatusError = createError('TokenWrongStatusError', 'The current status of the token does not allow this operation');
71
73
  export const TokenNotFoundError = createError('TokenNotFoundError', 'Token not found');
package/lib/events.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export declare const API_EVENTS: {
2
2
  readonly API_LOGIN_TOKEN_CREATED: "API_LOGIN_TOKEN_CREATED";
3
+ readonly API_LOGIN_FAILED: "API_LOGIN_FAILED";
3
4
  readonly API_LOGOUT: "API_LOGOUT";
4
5
  readonly ACL_DENIED: "ACL_DENIED";
5
6
  readonly ACL_GRANTED_SENSITIVE: "ACL_GRANTED_SENSITIVE";
package/lib/events.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export const API_EVENTS = {
2
2
  API_LOGIN_TOKEN_CREATED: 'API_LOGIN_TOKEN_CREATED',
3
+ API_LOGIN_FAILED: 'API_LOGIN_FAILED',
3
4
  API_LOGOUT: 'API_LOGOUT',
4
5
  ACL_DENIED: 'ACL_DENIED',
5
6
  ACL_GRANTED_SENSITIVE: 'ACL_GRANTED_SENSITIVE',
@@ -43,6 +43,16 @@ const setupMCPChatHandler = (chatConfiguration) => {
43
43
  res.status(405).json({ error: 'Method Not Allowed. Use POST.' });
44
44
  return;
45
45
  }
46
+ const unchainedContext = req.unchainedContext;
47
+ const user = unchainedContext?.user;
48
+ if (!user) {
49
+ res.status(401).json({ error: 'unauthorized' });
50
+ return;
51
+ }
52
+ if (!(user.roles || []).includes('admin')) {
53
+ res.status(403).json({ error: 'forbidden', message: 'Chat requires admin privileges' });
54
+ return;
55
+ }
46
56
  let client;
47
57
  const lifecycle = createChatRequestLifecycle(res, configuredAbortSignal);
48
58
  try {
@@ -52,13 +62,14 @@ const setupMCPChatHandler = (chatConfiguration) => {
52
62
  url: unchainedMCPUrl,
53
63
  headers: {
54
64
  Cookie: req.headers.cookie || '',
65
+ ...(req.headers.authorization ? { Authorization: req.headers.authorization } : {}),
55
66
  },
56
67
  },
57
68
  initializationOptions: { signal: lifecycle.signal },
58
69
  });
59
70
  lifecycle.setClientClose(() => client.close());
60
71
  const defaultUnchainedTools = await client.tools();
61
- const resourceContext = await buildChatResourceContext(req.unchainedContext);
72
+ const resourceContext = await buildChatResourceContext(unchainedContext);
62
73
  const tools = {
63
74
  ...defaultUnchainedTools,
64
75
  ...additionalTools,
@@ -4,10 +4,8 @@ import type { UnchainedCore } from '@unchainedshop/core';
4
4
  import type { AuthConfig } from '../auth.ts';
5
5
  import type { ChatConfiguration } from '../chat/utils.ts';
6
6
  import { connectChat } from './chatHandler.ts';
7
- import { type AdminUIThemeConfig } from '@unchainedshop/admin-ui/theme';
8
- import { type AdminUIPluginConfig } from '../adminUiPlugins.ts';
9
- export type { AdminUIPluginConfig, AdminUIPluginEntityConfig, AdminUIPluginPageConfig, AdminUIPluginTabConfig, AdminUIPluginWidgetConfig, AdminUIPluginSlotConfig, } from '../adminUiPlugins.ts';
10
- export type { AdminUIThemeTokens, AdminUIThemeConfig } from '@unchainedshop/admin-ui/theme';
7
+ import { type AdminUIPluginConfig, type AdminUIThemeConfig } from '../adminUiPlugins.ts';
8
+ export type { AdminUIPluginConfig, AdminUIPluginEntityConfig, AdminUIPluginPageConfig, AdminUIPluginTabConfig, AdminUIPluginWidgetConfig, AdminUIPluginSlotConfig, AdminUIThemeTokens, AdminUIThemeConfig, } from '../adminUiPlugins.ts';
11
9
  export interface AdminUIRouterOptions {
12
10
  prefix?: string;
13
11
  enabled?: boolean;