@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.
- package/README.md +2 -2
- package/lib/acl.js +4 -0
- package/lib/adminUiPlugins.d.ts +36 -1
- package/lib/adminUiPlugins.js +188 -26
- package/lib/auth.d.ts +0 -8
- package/lib/auth.js +0 -20
- package/lib/errors.d.ts +2 -0
- package/lib/errors.js +2 -0
- package/lib/events.d.ts +1 -0
- package/lib/events.js +1 -0
- package/lib/express/chatHandler.js +12 -1
- package/lib/express/index.d.ts +2 -4
- package/lib/express/index.js +63 -24
- package/lib/fastify/chatHandler.js +10 -1
- package/lib/fastify/index.d.ts +4 -4
- package/lib/fastify/index.js +83 -30
- package/lib/loaders/assortmentLinksLoader.js +24 -22
- package/lib/loaders/assortmentMediaTextLoader.js +1 -0
- package/lib/loaders/assortmentProductLoader.js +7 -4
- package/lib/loaders/assortmentTextLoader.js +1 -0
- package/lib/loaders/filterTextLoader.js +1 -0
- package/lib/loaders/productMediaTextLoader.js +1 -0
- package/lib/loaders/productTextLoader.js +1 -0
- package/lib/loaders/productVariationTextLoader.js +1 -0
- package/lib/mcp/tools/assortment/handlers/getAssortmentFilters.js +1 -1
- package/lib/mcp/tools/assortment/handlers/getAssortmentProducts.js +1 -1
- package/lib/mcp/tools/system/handlers/allocateWork.js +2 -2
- package/lib/mcp/tools/users/handlers/getUserPaymentCredentials.js +1 -0
- package/lib/mcp/tools/users/index.js +1 -1
- package/lib/mcp/utils/getNormalizedAssortmentDetails.js +2 -2
- package/lib/mcp/utils/getNormalizedUserDetails.js +1 -0
- package/lib/middleware/createAuthMiddleware.js +4 -39
- package/lib/resolvers/mutations/accounts/loginWithPassword.js +16 -2
- package/lib/resolvers/mutations/accounts/logoutAllSessions.d.ts +3 -1
- package/lib/resolvers/mutations/accounts/logoutAllSessions.js +10 -4
- package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.js +18 -0
- package/lib/resolvers/mutations/bulk/bulkOperation.d.ts +12 -0
- package/lib/resolvers/mutations/bulk/bulkOperation.js +14 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.d.ts +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.js +7 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveFilters.d.ts +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveFilters.js +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveProducts.d.ts +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveProducts.js +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveUsers.d.ts +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveUsers.js +7 -0
- package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.js +9 -0
- package/lib/resolvers/mutations/bulk/bulkSetFilterActive.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetFilterActive.js +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetProductStatus.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetProductStatus.js +17 -0
- package/lib/resolvers/mutations/bulk/bulkSetUserRoles.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetUserRoles.js +14 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.d.ts +11 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.js +10 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.d.ts +11 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.js +7 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.d.ts +11 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.js +7 -0
- package/lib/resolvers/mutations/index.d.ts +12 -0
- package/lib/resolvers/mutations/index.js +25 -1
- package/lib/resolvers/mutations/orders/checkoutCart.js +1 -1
- package/lib/resolvers/mutations/users/removeUser.js +9 -2
- package/lib/resolvers/mutations/users/setRoles.js +9 -2
- package/lib/resolvers/mutations/worker/allocateWork.d.ts +1 -1
- package/lib/resolvers/mutations/worker/allocateWork.js +4 -3
- package/lib/resolvers/queries/index.d.ts +1 -0
- package/lib/resolvers/queries/index.js +2 -0
- package/lib/resolvers/queries/search/globalSearch.d.ts +36 -0
- package/lib/resolvers/queries/search/globalSearch.js +200 -0
- package/lib/resolvers/type/assortment/assortment-types.js +2 -2
- package/lib/resolvers/type/global-search-result-types.d.ts +3 -0
- package/lib/resolvers/type/global-search-result-types.js +15 -0
- package/lib/resolvers/type/index.d.ts +3 -0
- package/lib/resolvers/type/index.js +2 -0
- package/lib/resolvers/type/user-types.d.ts +1 -0
- package/lib/resolvers/type/user-types.js +36 -4
- package/lib/roles/all.js +12 -0
- package/lib/roles/index.js +12 -0
- package/lib/roles/loggedIn.js +1 -1
- package/lib/schema/inputTypes.js +5 -0
- package/lib/schema/mutation.js +74 -1
- package/lib/schema/query.js +15 -0
- package/lib/schema/types/common.js +7 -0
- package/lib/schema/types/search.js +36 -0
- package/lib/schema/types/user.js +4 -0
- package/package.json +31 -26
- package/lib/adminUiTheme.d.ts +0 -25
- 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 | `
|
|
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
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;
|
|
@@ -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
|
|
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 {};
|
package/lib/adminUiPlugins.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
3
|
-
import {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
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
|
|
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
|
|
63
|
-
|
|
64
|
-
if (
|
|
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
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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: '
|
|
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: '
|
|
261
|
+
contentType: 'text/javascript',
|
|
159
262
|
cacheControl: IMMUTABLE_CACHE,
|
|
160
263
|
});
|
|
161
264
|
}
|
|
162
265
|
}
|
|
163
|
-
|
|
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
|
@@ -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(
|
|
72
|
+
const resourceContext = await buildChatResourceContext(unchainedContext);
|
|
62
73
|
const tools = {
|
|
63
74
|
...defaultUnchainedTools,
|
|
64
75
|
...additionalTools,
|
package/lib/express/index.d.ts
CHANGED
|
@@ -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 '
|
|
8
|
-
|
|
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;
|