@unchainedshop/api 5.0.0-alpha.4 → 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 (68) hide show
  1. package/lib/acl.js +4 -0
  2. package/lib/adminUiPlugins.d.ts +12 -1
  3. package/lib/adminUiPlugins.js +188 -26
  4. package/lib/errors.d.ts +2 -0
  5. package/lib/errors.js +2 -0
  6. package/lib/events.d.ts +1 -0
  7. package/lib/events.js +1 -0
  8. package/lib/express/index.js +44 -17
  9. package/lib/fastify/index.d.ts +2 -0
  10. package/lib/fastify/index.js +65 -24
  11. package/lib/mcp/tools/system/handlers/allocateWork.js +2 -2
  12. package/lib/mcp/tools/users/index.js +1 -1
  13. package/lib/middleware/createAuthMiddleware.js +2 -0
  14. package/lib/resolvers/mutations/accounts/loginWithPassword.js +16 -2
  15. package/lib/resolvers/mutations/accounts/logoutAllSessions.d.ts +3 -1
  16. package/lib/resolvers/mutations/accounts/logoutAllSessions.js +10 -4
  17. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.d.ts +10 -0
  18. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.js +18 -0
  19. package/lib/resolvers/mutations/bulk/bulkOperation.d.ts +12 -0
  20. package/lib/resolvers/mutations/bulk/bulkOperation.js +14 -0
  21. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.d.ts +9 -0
  22. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.js +7 -0
  23. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.d.ts +9 -0
  24. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.js +9 -0
  25. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.d.ts +9 -0
  26. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.js +9 -0
  27. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.d.ts +9 -0
  28. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.js +7 -0
  29. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.d.ts +10 -0
  30. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.js +9 -0
  31. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.d.ts +10 -0
  32. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.js +10 -0
  33. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.d.ts +10 -0
  34. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.js +17 -0
  35. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.d.ts +10 -0
  36. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.js +14 -0
  37. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.d.ts +11 -0
  38. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.js +10 -0
  39. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.d.ts +11 -0
  40. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.js +7 -0
  41. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.d.ts +11 -0
  42. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.js +7 -0
  43. package/lib/resolvers/mutations/index.d.ts +12 -0
  44. package/lib/resolvers/mutations/index.js +25 -1
  45. package/lib/resolvers/mutations/users/removeUser.js +9 -2
  46. package/lib/resolvers/mutations/users/setRoles.js +9 -2
  47. package/lib/resolvers/mutations/worker/allocateWork.d.ts +1 -1
  48. package/lib/resolvers/mutations/worker/allocateWork.js +4 -3
  49. package/lib/resolvers/queries/index.d.ts +1 -0
  50. package/lib/resolvers/queries/index.js +2 -0
  51. package/lib/resolvers/queries/search/globalSearch.d.ts +36 -0
  52. package/lib/resolvers/queries/search/globalSearch.js +200 -0
  53. package/lib/resolvers/type/global-search-result-types.d.ts +3 -0
  54. package/lib/resolvers/type/global-search-result-types.js +15 -0
  55. package/lib/resolvers/type/index.d.ts +3 -0
  56. package/lib/resolvers/type/index.js +2 -0
  57. package/lib/resolvers/type/user-types.d.ts +1 -0
  58. package/lib/resolvers/type/user-types.js +35 -4
  59. package/lib/roles/all.js +12 -0
  60. package/lib/roles/index.js +12 -0
  61. package/lib/roles/loggedIn.js +1 -1
  62. package/lib/schema/inputTypes.js +5 -0
  63. package/lib/schema/mutation.js +74 -1
  64. package/lib/schema/query.js +15 -0
  65. package/lib/schema/types/common.js +7 -0
  66. package/lib/schema/types/search.js +36 -0
  67. package/lib/schema/types/user.js +4 -0
  68. package/package.json +25 -25
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,
@@ -85,13 +85,24 @@ interface StaticAsset {
85
85
  export interface PreparedPluginAssets {
86
86
  routes: Map<string, StaticAsset>;
87
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;
88
95
  }
89
96
  export declare const resolveAdminUIPath: () => string | null;
90
- 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;
91
101
  export declare function preparePluginAssets(plugins: AdminUIPluginConfig[], log: {
92
102
  info: (...args: any[]) => void;
93
103
  warn: (...args: any[]) => void;
94
104
  }, options?: {
95
105
  devMode?: boolean;
96
106
  }): PreparedPluginAssets;
107
+ export declare function buildImportMapTag(importMapJSON: string, nonce?: string): string;
97
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/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',
@@ -1,16 +1,23 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
1
9
  import e from 'express';
2
10
  import cookieParser from 'cookie-parser';
3
11
  import { pluginRegistry } from '@unchainedshop/core';
4
12
  import { createHash } from 'node:crypto';
5
- import { existsSync } from 'node:fs';
6
- import { join } from 'node:path';
13
+ import { runWithAuditContext } from '@unchainedshop/events';
7
14
  import { getCurrentContextResolver } from "../context.js";
8
15
  import { createAuthContext } from "../middleware/createAuthMiddleware.js";
9
16
  import createMCPMiddleware from "./createMCPMiddleware.js";
10
17
  import { connectChat } from "./chatHandler.js";
11
18
  import { mountRoutes } from "./mountRoutes.js";
12
19
  import { createBackchannelLogoutRoute } from "../handlers/createBackchannelLogoutHandler.js";
13
- import { preparePluginAssets, resolveAdminUIPath, } from "../adminUiPlugins.js";
20
+ import { preparePluginAssets, prepareAdminUIHTML, resolveAdminUIHTML, injectAdminUIImportMap, buildImportMapTag, resolveAdminUIPath, } from "../adminUiPlugins.js";
14
21
  export const adminUIRouter = (enabled = true, theme, plugins = []) => {
15
22
  const router = e.Router();
16
23
  const adminUIPath = resolveAdminUIPath();
@@ -22,7 +29,8 @@ export const adminUIRouter = (enabled = true, theme, plugins = []) => {
22
29
  let css = '/* default theme */';
23
30
  if (theme) {
24
31
  try {
25
- const { generateThemeCSS } = await import('@unchainedshop/admin-ui/theme');
32
+ const themeModuleUrl = import.meta.resolve('@unchainedshop/admin-ui/theme');
33
+ const { generateThemeCSS } = await import(__rewriteRelativeImportExtension(themeModuleUrl));
26
34
  css = generateThemeCSS(theme);
27
35
  }
28
36
  catch {
@@ -45,7 +53,7 @@ export const adminUIRouter = (enabled = true, theme, plugins = []) => {
45
53
  });
46
54
  const log = { info: console.info, warn: console.warn };
47
55
  const devMode = process.env.NODE_ENV !== 'production';
48
- const { routes: pluginRoutes } = preparePluginAssets(plugins, log, { devMode });
56
+ const { routes: pluginRoutes, importMapTag, importMapJSON, } = preparePluginAssets(plugins, log, { devMode });
49
57
  for (const [path, asset] of pluginRoutes) {
50
58
  router.get(path, (req, res) => {
51
59
  if (asset.etag) {
@@ -63,16 +71,23 @@ export const adminUIRouter = (enabled = true, theme, plugins = []) => {
63
71
  .send(body);
64
72
  });
65
73
  }
66
- router.use(e.static(adminUIPath));
67
- const extHtmlPath = join(adminUIPath, 'ext', '[[...slug]]', 'index.html');
68
- const hasExtHtml = existsSync(extHtmlPath);
69
- router.get(/(.*)/, (req, res) => {
70
- const urlPath = req.path.replace(/\/+$/, '');
71
- if (hasExtHtml && (urlPath === '/ext' || urlPath.startsWith('/ext/'))) {
72
- return res.sendFile(extHtmlPath);
73
- }
74
- return res.sendFile(join(adminUIPath, 'index.html'));
75
- });
74
+ router.use(e.static(adminUIPath, importMapTag ? { index: false } : undefined));
75
+ if (importMapJSON) {
76
+ const preparedHTML = prepareAdminUIHTML(adminUIPath);
77
+ router.get(/(.*)/, (req, res) => {
78
+ if (devMode)
79
+ res.set('Cache-Control', 'no-cache');
80
+ const nonce = res.locals?.cspNonce;
81
+ const tag = nonce ? buildImportMapTag(importMapJSON, nonce) : importMapTag;
82
+ return res.type('text/html').send(injectAdminUIImportMap(preparedHTML, req.path, tag));
83
+ });
84
+ }
85
+ else {
86
+ const preparedHTML = prepareAdminUIHTML(adminUIPath);
87
+ router.get(/(.*)/, (req, res) => {
88
+ return res.type('text/html').send(resolveAdminUIHTML(preparedHTML, req.path));
89
+ });
90
+ }
76
91
  }
77
92
  return router;
78
93
  };
@@ -112,7 +127,7 @@ const createAddContextMiddleware = (authConfig, trustProxy = false) => async fun
112
127
  };
113
128
  const authContext = await createAuthContext(authContextParams, authConfig);
114
129
  const context = getCurrentContextResolver();
115
- req.unchainedContext = await context({
130
+ const unchainedContext = await context({
116
131
  setHeader,
117
132
  getHeader,
118
133
  remoteAddress,
@@ -124,6 +139,12 @@ const createAddContextMiddleware = (authConfig, trustProxy = false) => async fun
124
139
  impersonatorId: authContext.impersonatorId,
125
140
  tokenVersion: authContext.tokenVersion,
126
141
  }, req, res);
142
+ req.unchainedContext = unchainedContext;
143
+ req._auditContext = {
144
+ userId: unchainedContext.userId,
145
+ userName: unchainedContext.user?.username || unchainedContext.user?.emails?.[0]?.address,
146
+ remoteAddress,
147
+ };
127
148
  next();
128
149
  }
129
150
  catch (error) {
@@ -152,7 +173,13 @@ export const connect = async (expressApp, { graphqlHandler, unchainedAPI, }, { a
152
173
  }
153
174
  expressApp.use(cookieParser());
154
175
  expressApp.use(createAddContextMiddleware(authConfig, trustProxy || allowRemoteToLocalhostSecureCookies));
155
- expressApp.use(graphqlHandler.graphqlEndpoint, graphqlHandler.handle);
176
+ expressApp.use(graphqlHandler.graphqlEndpoint, (req, res) => {
177
+ const auditCtx = req._auditContext;
178
+ if (auditCtx) {
179
+ return runWithAuditContext(auditCtx, () => graphqlHandler.handle(req, res));
180
+ }
181
+ return graphqlHandler.handle(req, res);
182
+ });
156
183
  expressApp.use(MCP_API_PATH, e.json({ limit: '10mb' }));
157
184
  expressApp.use(MCP_API_PATH, createMCPMiddleware);
158
185
  if (chat) {
@@ -11,6 +11,8 @@ export interface AdminUIRouterOptions {
11
11
  enabled?: boolean;
12
12
  theme?: AdminUIThemeConfig;
13
13
  plugins?: AdminUIPluginConfig[];
14
+ importMapTag?: string | null;
15
+ importMapJSON?: string | null;
14
16
  }
15
17
  export declare const unchainedLogger: (prefix: string) => FastifyBaseLogger;
16
18
  export declare const connect: (fastify: FastifyInstance, { graphqlHandler, unchainedAPI, }: {
@@ -1,3 +1,12 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { runWithAuditContext } from '@unchainedshop/events';
1
10
  import { getCurrentContextResolver } from "../context.js";
2
11
  import { createAuthContext } from "../middleware/createAuthMiddleware.js";
3
12
  import { pluginRegistry } from '@unchainedshop/core';
@@ -7,10 +16,9 @@ import mcpHandler from "./mcpHandler.js";
7
16
  import { connectChat } from "./chatHandler.js";
8
17
  import { mountRoutes } from "./mountRoutes.js";
9
18
  import { createHash } from 'node:crypto';
10
- import { existsSync, readFileSync } from 'node:fs';
11
- import { join } from 'node:path';
19
+ import { readFileSync } from 'node:fs';
12
20
  import { createBackchannelLogoutRoute } from "../handlers/createBackchannelLogoutHandler.js";
13
- import { preparePluginAssets, resolveAdminUIPath, } from "../adminUiPlugins.js";
21
+ import { preparePluginAssets, prepareAdminUIHTML, resolveAdminUIHTML, injectAdminUIImportMap, buildImportMapTag, resolveAdminUIPath, } from "../adminUiPlugins.js";
14
22
  const resolveUserRemoteAddress = (req, trustProxy = false) => {
15
23
  let remoteAddress;
16
24
  if (trustProxy) {
@@ -46,7 +54,7 @@ const createMiddlewareHook = (authConfig, trustProxy = false) => async function
46
54
  };
47
55
  const authContext = await createAuthContext(authContextParams, authConfig);
48
56
  const context = getCurrentContextResolver();
49
- req.unchainedContext = await context({
57
+ const unchainedContext = await context({
50
58
  setHeader,
51
59
  getHeader,
52
60
  remoteAddress,
@@ -58,6 +66,12 @@ const createMiddlewareHook = (authConfig, trustProxy = false) => async function
58
66
  impersonatorId: authContext.impersonatorId,
59
67
  tokenVersion: authContext.tokenVersion,
60
68
  }, req, reply);
69
+ req.unchainedContext = unchainedContext;
70
+ req._auditContext = {
71
+ userId: unchainedContext.userId,
72
+ userName: unchainedContext.user?.username || unchainedContext.user?.emails?.[0]?.address,
73
+ remoteAddress,
74
+ };
61
75
  };
62
76
  export const unchainedLogger = (prefix) => {
63
77
  const logger = createLogger(prefix);
@@ -103,10 +117,15 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
103
117
  url: graphqlHandler.graphqlEndpoint,
104
118
  method: ['GET', 'POST', 'OPTIONS'],
105
119
  handler: async (req, reply) => {
106
- return graphqlHandler.handleNodeRequestAndResponse(req, reply, {
120
+ const auditCtx = req._auditContext;
121
+ const handle = () => graphqlHandler.handleNodeRequestAndResponse(req, reply, {
107
122
  req,
108
123
  reply,
109
124
  });
125
+ if (auditCtx) {
126
+ return runWithAuditContext(auditCtx, handle);
127
+ }
128
+ return handle();
110
129
  },
111
130
  });
112
131
  fastify.route({
@@ -130,7 +149,8 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
130
149
  let css = '/* default theme */';
131
150
  if (adminUIOptions?.theme) {
132
151
  try {
133
- const { generateThemeCSS } = await import('@unchainedshop/admin-ui/theme');
152
+ const themeModuleUrl = import.meta.resolve('@unchainedshop/admin-ui/theme');
153
+ const { generateThemeCSS } = await import(__rewriteRelativeImportExtension(themeModuleUrl));
134
154
  css = generateThemeCSS(adminUIOptions.theme);
135
155
  }
136
156
  catch {
@@ -152,7 +172,7 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
152
172
  .send(css);
153
173
  });
154
174
  const devMode = process.env.NODE_ENV !== 'production';
155
- const { routes: pluginRoutes } = preparePluginAssets(adminUIPlugins, fastify.log, {
175
+ const { routes: pluginRoutes, importMapTag, importMapJSON, } = preparePluginAssets(adminUIPlugins, fastify.log, {
156
176
  devMode,
157
177
  });
158
178
  for (const [path, asset] of pluginRoutes) {
@@ -177,6 +197,8 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
177
197
  enabled: true,
178
198
  prefix: adminUIOptions?.prefix || '/',
179
199
  plugins: adminUIPlugins,
200
+ importMapTag,
201
+ importMapJSON,
180
202
  });
181
203
  }
182
204
  };
@@ -209,24 +231,43 @@ export const adminUIRouter = async (fastify, opts) => {
209
231
  if (fastifyStatic) {
210
232
  const adminUIPath = resolveAdminUIPath();
211
233
  if (adminUIPath) {
212
- const indexHtml = readFileSync(join(adminUIPath, 'index.html'), 'utf-8');
213
- const extHtmlPath = join(adminUIPath, 'ext', '[[...slug]]', 'index.html');
214
- const extHtml = existsSync(extHtmlPath) ? readFileSync(extHtmlPath, 'utf-8') : null;
215
- await fastify.register(fastifyStatic, {
216
- root: adminUIPath,
217
- prefix: opts.prefix || '/',
218
- wildcard: false,
219
- });
220
- fastify.setNotFoundHandler(async (request, reply) => {
221
- if (request.method === 'GET' && !request.url.includes('.')) {
222
- const urlPath = request.url.split('?')[0].replace(/\/+$/, '');
223
- if (extHtml && (urlPath === '/ext' || urlPath.startsWith('/ext/'))) {
224
- return reply.type('text/html').send(extHtml);
234
+ if (opts.importMapJSON) {
235
+ const preparedHTML = prepareAdminUIHTML(adminUIPath);
236
+ await fastify.register(fastifyStatic, {
237
+ root: adminUIPath,
238
+ prefix: opts.prefix || '/',
239
+ wildcard: false,
240
+ index: false,
241
+ });
242
+ fastify.setNotFoundHandler(async (request, reply) => {
243
+ if (request.method === 'GET' && !request.url.includes('.')) {
244
+ if (process.env.NODE_ENV !== 'production')
245
+ reply.header('Cache-Control', 'no-cache');
246
+ const nonce = reply.cspNonce?.script;
247
+ const tag = nonce ? buildImportMapTag(opts.importMapJSON, nonce) : opts.importMapTag;
248
+ return reply
249
+ .type('text/html')
250
+ .send(injectAdminUIImportMap(preparedHTML, request.url, tag, opts.prefix));
225
251
  }
226
- return reply.type('text/html').send(indexHtml);
227
- }
228
- return reply.code(404).send({ error: 'Not Found' });
229
- });
252
+ return reply.code(404).send({ error: 'Not Found' });
253
+ });
254
+ }
255
+ else {
256
+ const preparedHTML = prepareAdminUIHTML(adminUIPath);
257
+ await fastify.register(fastifyStatic, {
258
+ root: adminUIPath,
259
+ prefix: opts.prefix || '/',
260
+ wildcard: false,
261
+ });
262
+ fastify.setNotFoundHandler(async (request, reply) => {
263
+ if (request.method === 'GET' && !request.url.includes('.')) {
264
+ return reply
265
+ .type('text/html')
266
+ .send(resolveAdminUIHTML(preparedHTML, request.url, opts.prefix));
267
+ }
268
+ return reply.code(404).send({ error: 'Not Found' });
269
+ });
270
+ }
230
271
  return;
231
272
  }
232
273
  }
@@ -1,8 +1,8 @@
1
1
  import { buildObfuscatedFieldsFilter } from '@unchainedshop/utils';
2
+ import { WorkerDirector } from '@unchainedshop/core';
2
3
  const allocateWork = async (context, options) => {
3
- const { modules } = context;
4
4
  const removePrivateFieldsFromWork = buildObfuscatedFieldsFilter(context.options.worker?.blacklistedVariables);
5
- const work = await modules.worker.allocateWork({
5
+ const work = await WorkerDirector.allocateWork(context, {
6
6
  types: options.types || [],
7
7
  worker: options.worker || '',
8
8
  });