@unchainedshop/api 4.8.17 → 4.8.19

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.
@@ -0,0 +1,70 @@
1
+ export interface AdminUIPluginEntityConfig {
2
+ path: string;
3
+ label: string;
4
+ icon?: string;
5
+ requiredRole?: string;
6
+ components: {
7
+ list: string;
8
+ detail: string;
9
+ create?: string;
10
+ };
11
+ }
12
+ export interface AdminUIPluginPageConfig {
13
+ path: string;
14
+ label: string;
15
+ icon?: string;
16
+ requiredRole?: string;
17
+ component: string;
18
+ }
19
+ export interface AdminUIPluginTabConfig {
20
+ label: string;
21
+ component: string;
22
+ requiredRole?: string;
23
+ }
24
+ export interface AdminUIPluginWidgetConfig {
25
+ component: string;
26
+ width?: 'full' | 'half' | 'third';
27
+ }
28
+ export interface AdminUIPluginSlotConfig {
29
+ component: string;
30
+ }
31
+ export interface AdminUIPluginConfig {
32
+ name: string;
33
+ version?: string;
34
+ bundlePath: string;
35
+ navigation?: {
36
+ label: string;
37
+ icon?: string;
38
+ requiredRole?: string;
39
+ };
40
+ slots: {
41
+ entities?: AdminUIPluginEntityConfig[];
42
+ pages?: AdminUIPluginPageConfig[];
43
+ 'dashboard:widgets'?: AdminUIPluginWidgetConfig[];
44
+ 'product:tabs'?: AdminUIPluginTabConfig[];
45
+ 'assortment:tabs'?: AdminUIPluginTabConfig[];
46
+ 'filter:tabs'?: AdminUIPluginTabConfig[];
47
+ 'user:tabs'?: AdminUIPluginTabConfig[];
48
+ 'order:tabs'?: AdminUIPluginTabConfig[];
49
+ [key: string]: AdminUIPluginTabConfig[] | AdminUIPluginSlotConfig[] | AdminUIPluginEntityConfig[] | AdminUIPluginPageConfig[] | AdminUIPluginWidgetConfig[] | undefined;
50
+ };
51
+ }
52
+ interface StaticAsset {
53
+ content: string | (() => string);
54
+ contentType: string;
55
+ cacheControl: string;
56
+ etag?: string;
57
+ }
58
+ export interface PreparedPluginAssets {
59
+ routes: Map<string, StaticAsset>;
60
+ validPlugins: AdminUIPluginConfig[];
61
+ importMapTag: string | null;
62
+ }
63
+ export declare const resolveAdminUIPath: () => string | null;
64
+ export declare function preparePluginAssets(plugins: AdminUIPluginConfig[], log: {
65
+ info: (...args: any[]) => void;
66
+ warn: (...args: any[]) => void;
67
+ }, options?: {
68
+ devMode?: boolean;
69
+ }): PreparedPluginAssets;
70
+ export {};
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ const PLUGIN_NAME_RE = /^[a-z0-9]([a-z0-9_-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9_-]*[a-z0-9])?)*$/i;
5
+ const IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';
6
+ const SDK_FILES = ['ui.mjs', 'form.mjs', 'hooks.mjs', 'providers.mjs', 'modal.mjs'];
7
+ const IMPORT_MAP = {
8
+ imports: {
9
+ '@unchainedshop/admin-ui/ui': '/admin-ui-sdk/ui.mjs',
10
+ '@unchainedshop/admin-ui/form': '/admin-ui-sdk/form.mjs',
11
+ '@unchainedshop/admin-ui/hooks': '/admin-ui-sdk/hooks.mjs',
12
+ '@unchainedshop/admin-ui/modal': '/admin-ui-sdk/modal.mjs',
13
+ '@unchainedshop/admin-ui/providers': '/admin-ui-sdk/providers.mjs',
14
+ },
15
+ };
16
+ export const resolveAdminUIPath = () => {
17
+ try {
18
+ const staticURL = import.meta.resolve('@unchainedshop/admin-ui');
19
+ return new URL(staticURL).pathname.split('/').slice(0, -1).join('/');
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ };
25
+ export function preparePluginAssets(plugins, log, options = {}) {
26
+ const { devMode = false } = options;
27
+ const routes = new Map();
28
+ const validPlugins = plugins.filter((p) => {
29
+ if (!PLUGIN_NAME_RE.test(p.name)) {
30
+ log.warn(`Skipping admin-ui plugin with invalid name: "${p.name}"`);
31
+ return false;
32
+ }
33
+ return true;
34
+ });
35
+ if (validPlugins.length > 0) {
36
+ const pluginList = validPlugins
37
+ .map((p) => `${p.name}${p.version ? `@${p.version}` : ''}`)
38
+ .join(', ');
39
+ log.info(`Loading ${validPlugins.length} admin-ui plugin(s): ${pluginList}`);
40
+ }
41
+ const contentHash = (content) => createHash('sha256').update(content).digest('hex').slice(0, 8);
42
+ const pluginBundles = new Map();
43
+ for (const plugin of validPlugins) {
44
+ try {
45
+ const content = readFileSync(resolve(plugin.bundlePath), 'utf-8');
46
+ pluginBundles.set(plugin.name, { content, hash: contentHash(content) });
47
+ }
48
+ catch (err) {
49
+ log.warn(`Failed to read bundle for plugin "${plugin.name}" at ${plugin.bundlePath}: ${err.message}`);
50
+ }
51
+ }
52
+ const manifestJSON = JSON.stringify(validPlugins
53
+ .filter((p) => pluginBundles.has(p.name))
54
+ .map((plugin) => {
55
+ const bundle = pluginBundles.get(plugin.name);
56
+ return {
57
+ ...Object.fromEntries(Object.entries(plugin).filter(([k]) => k !== 'bundlePath')),
58
+ bundleUrl: `/admin-plugins/${plugin.name}.js?v=${bundle.hash}`,
59
+ };
60
+ }));
61
+ const devCacheControl = 'no-cache, no-store, must-revalidate';
62
+ const manifestHash = contentHash(manifestJSON);
63
+ routes.set('/admin-ui-plugins.json', {
64
+ content: devMode ? () => manifestJSON : manifestJSON,
65
+ contentType: 'application/json',
66
+ cacheControl: devMode ? devCacheControl : `public, max-age=0, must-revalidate`,
67
+ etag: `"${manifestHash}"`,
68
+ });
69
+ for (const plugin of validPlugins.filter((p) => pluginBundles.has(p.name))) {
70
+ const bundlePath = resolve(plugin.bundlePath);
71
+ if (devMode) {
72
+ routes.set(`/admin-plugins/${plugin.name}.js`, {
73
+ content: () => readFileSync(bundlePath, 'utf-8'),
74
+ contentType: 'application/javascript',
75
+ cacheControl: devCacheControl,
76
+ });
77
+ }
78
+ else {
79
+ const { content } = pluginBundles.get(plugin.name);
80
+ routes.set(`/admin-plugins/${plugin.name}.js`, {
81
+ content,
82
+ contentType: 'application/javascript',
83
+ cacheControl: IMMUTABLE_CACHE,
84
+ });
85
+ }
86
+ }
87
+ let importMapTag = null;
88
+ if (pluginBundles.size > 0) {
89
+ const adminUIPath = resolveAdminUIPath();
90
+ if (adminUIPath) {
91
+ for (const file of SDK_FILES) {
92
+ const sdkPath = join(adminUIPath, '..', 'dist', file);
93
+ if (existsSync(sdkPath)) {
94
+ routes.set(`/admin-ui-sdk/${file}`, {
95
+ content: readFileSync(sdkPath, 'utf-8'),
96
+ contentType: 'application/javascript',
97
+ cacheControl: IMMUTABLE_CACHE,
98
+ });
99
+ }
100
+ }
101
+ const importMapJSON = JSON.stringify(IMPORT_MAP);
102
+ routes.set('/admin-ui-importmap.json', {
103
+ content: importMapJSON,
104
+ contentType: 'application/json',
105
+ cacheControl: IMMUTABLE_CACHE,
106
+ });
107
+ importMapTag = `<script type="importmap">${importMapJSON}</script>`;
108
+ }
109
+ }
110
+ return { routes, validPlugins, importMapTag };
111
+ }
@@ -2,11 +2,277 @@ import type * as aiTypes from 'ai';
2
2
  declare const generateImageHandler: (req: any) => ({ model, uploadUrl, }: {
3
3
  model: aiTypes.ImageModel;
4
4
  uploadUrl?: string;
5
- }) => aiTypes.Tool<{
6
- prompt: string;
7
- size?: "512x512" | "768x768" | "1024x1024" | "512x896" | "640x1120" | "768x1344" | "1024x1792" | "896x512" | "1120x640" | "1344x768" | "1792x1024" | undefined;
8
- }, string | {
9
- imageUrl: any;
10
- prompt: string;
11
- }>;
5
+ }) => ({
6
+ title?: string;
7
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
8
+ metadata?: import("@ai-sdk/provider").JSONObject;
9
+ inputSchema: aiTypes.FlexibleSchema<{
10
+ prompt: any;
11
+ size?: string | undefined;
12
+ }>;
13
+ contextSchema?: aiTypes.FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
14
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
15
+ prompt: any;
16
+ size?: string | undefined;
17
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
18
+ onInputStart?: ((options: aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
19
+ onInputDelta?: ((options: {
20
+ inputTextDelta: string;
21
+ } & aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
22
+ onInputAvailable?: ((options: {
23
+ input: {
24
+ prompt: any;
25
+ size?: string | undefined;
26
+ };
27
+ } & aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
28
+ toModelOutput?: ((options: {
29
+ toolCallId: string;
30
+ input: {
31
+ prompt: any;
32
+ size?: string | undefined;
33
+ };
34
+ output: NoInfer<string | {
35
+ imageUrl: any;
36
+ prompt: any;
37
+ }>;
38
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
39
+ } & {
40
+ outputSchema?: aiTypes.FlexibleSchema<string | {
41
+ imageUrl: any;
42
+ prompt: any;
43
+ }> | undefined;
44
+ execute: aiTypes.ToolExecuteFunction<{
45
+ prompt: any;
46
+ size?: string | undefined;
47
+ }, string | {
48
+ imageUrl: any;
49
+ prompt: any;
50
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
51
+ } & {
52
+ description?: string | ((options: {
53
+ context: NoInfer<import("@ai-sdk/provider-utils").Context>;
54
+ experimental_sandbox?: aiTypes.Experimental_SandboxSession;
55
+ }) => string) | undefined;
56
+ strict?: boolean;
57
+ inputExamples?: {
58
+ input: NoInfer<{
59
+ prompt: any;
60
+ size?: string | undefined;
61
+ }>;
62
+ }[] | undefined;
63
+ id?: never;
64
+ isProviderExecuted?: never;
65
+ args?: never;
66
+ supportsDeferredResults?: never;
67
+ } & {
68
+ type?: undefined | "function";
69
+ } & {
70
+ execute: aiTypes.ToolExecuteFunction<{
71
+ prompt: any;
72
+ size?: string | undefined;
73
+ }, string | {
74
+ imageUrl: any;
75
+ prompt: any;
76
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
77
+ }) | ({
78
+ title?: string;
79
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
80
+ metadata?: import("@ai-sdk/provider").JSONObject;
81
+ inputSchema: aiTypes.FlexibleSchema<{
82
+ prompt: any;
83
+ size?: string | undefined;
84
+ }>;
85
+ contextSchema?: aiTypes.FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
86
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
87
+ prompt: any;
88
+ size?: string | undefined;
89
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
90
+ onInputStart?: ((options: aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
91
+ onInputDelta?: ((options: {
92
+ inputTextDelta: string;
93
+ } & aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
94
+ onInputAvailable?: ((options: {
95
+ input: {
96
+ prompt: any;
97
+ size?: string | undefined;
98
+ };
99
+ } & aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
100
+ toModelOutput?: ((options: {
101
+ toolCallId: string;
102
+ input: {
103
+ prompt: any;
104
+ size?: string | undefined;
105
+ };
106
+ output: NoInfer<string | {
107
+ imageUrl: any;
108
+ prompt: any;
109
+ }>;
110
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
111
+ } & {
112
+ outputSchema?: aiTypes.FlexibleSchema<string | {
113
+ imageUrl: any;
114
+ prompt: any;
115
+ }> | undefined;
116
+ execute: aiTypes.ToolExecuteFunction<{
117
+ prompt: any;
118
+ size?: string | undefined;
119
+ }, string | {
120
+ imageUrl: any;
121
+ prompt: any;
122
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
123
+ } & {
124
+ description?: string | ((options: {
125
+ context: NoInfer<import("@ai-sdk/provider-utils").Context>;
126
+ experimental_sandbox?: aiTypes.Experimental_SandboxSession;
127
+ }) => string) | undefined;
128
+ strict?: boolean;
129
+ inputExamples?: {
130
+ input: NoInfer<{
131
+ prompt: any;
132
+ size?: string | undefined;
133
+ }>;
134
+ }[] | undefined;
135
+ id?: never;
136
+ isProviderExecuted?: never;
137
+ args?: never;
138
+ supportsDeferredResults?: never;
139
+ } & {
140
+ type: "dynamic";
141
+ } & {
142
+ execute: aiTypes.ToolExecuteFunction<{
143
+ prompt: any;
144
+ size?: string | undefined;
145
+ }, string | {
146
+ imageUrl: any;
147
+ prompt: any;
148
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
149
+ }) | ({
150
+ title?: string;
151
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
152
+ metadata?: import("@ai-sdk/provider").JSONObject;
153
+ inputSchema: aiTypes.FlexibleSchema<{
154
+ prompt: any;
155
+ size?: string | undefined;
156
+ }>;
157
+ contextSchema?: aiTypes.FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
158
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
159
+ prompt: any;
160
+ size?: string | undefined;
161
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
162
+ onInputStart?: ((options: aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
163
+ onInputDelta?: ((options: {
164
+ inputTextDelta: string;
165
+ } & aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
166
+ onInputAvailable?: ((options: {
167
+ input: {
168
+ prompt: any;
169
+ size?: string | undefined;
170
+ };
171
+ } & aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
172
+ toModelOutput?: ((options: {
173
+ toolCallId: string;
174
+ input: {
175
+ prompt: any;
176
+ size?: string | undefined;
177
+ };
178
+ output: NoInfer<string | {
179
+ imageUrl: any;
180
+ prompt: any;
181
+ }>;
182
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
183
+ } & {
184
+ outputSchema?: aiTypes.FlexibleSchema<string | {
185
+ imageUrl: any;
186
+ prompt: any;
187
+ }> | undefined;
188
+ execute: aiTypes.ToolExecuteFunction<{
189
+ prompt: any;
190
+ size?: string | undefined;
191
+ }, string | {
192
+ imageUrl: any;
193
+ prompt: any;
194
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
195
+ } & {
196
+ type: "provider";
197
+ id: `${string}.${string}`;
198
+ args: Record<string, unknown>;
199
+ description?: never;
200
+ strict?: never;
201
+ inputExamples?: never;
202
+ } & {
203
+ isProviderExecuted: false;
204
+ supportsDeferredResults?: never;
205
+ } & {
206
+ execute: aiTypes.ToolExecuteFunction<{
207
+ prompt: any;
208
+ size?: string | undefined;
209
+ }, string | {
210
+ imageUrl: any;
211
+ prompt: any;
212
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
213
+ }) | ({
214
+ title?: string;
215
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
216
+ metadata?: import("@ai-sdk/provider").JSONObject;
217
+ inputSchema: aiTypes.FlexibleSchema<{
218
+ prompt: any;
219
+ size?: string | undefined;
220
+ }>;
221
+ contextSchema?: aiTypes.FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
222
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
223
+ prompt: any;
224
+ size?: string | undefined;
225
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
226
+ onInputStart?: ((options: aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
227
+ onInputDelta?: ((options: {
228
+ inputTextDelta: string;
229
+ } & aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
230
+ onInputAvailable?: ((options: {
231
+ input: {
232
+ prompt: any;
233
+ size?: string | undefined;
234
+ };
235
+ } & aiTypes.ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
236
+ toModelOutput?: ((options: {
237
+ toolCallId: string;
238
+ input: {
239
+ prompt: any;
240
+ size?: string | undefined;
241
+ };
242
+ output: NoInfer<string | {
243
+ imageUrl: any;
244
+ prompt: any;
245
+ }>;
246
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
247
+ } & {
248
+ outputSchema?: aiTypes.FlexibleSchema<string | {
249
+ imageUrl: any;
250
+ prompt: any;
251
+ }> | undefined;
252
+ execute: aiTypes.ToolExecuteFunction<{
253
+ prompt: any;
254
+ size?: string | undefined;
255
+ }, string | {
256
+ imageUrl: any;
257
+ prompt: any;
258
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
259
+ } & {
260
+ type: "provider";
261
+ id: `${string}.${string}`;
262
+ args: Record<string, unknown>;
263
+ description?: never;
264
+ strict?: never;
265
+ inputExamples?: never;
266
+ } & {
267
+ isProviderExecuted: true;
268
+ supportsDeferredResults?: boolean;
269
+ } & {
270
+ execute: aiTypes.ToolExecuteFunction<{
271
+ prompt: any;
272
+ size?: string | undefined;
273
+ }, string | {
274
+ imageUrl: any;
275
+ prompt: any;
276
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
277
+ });
12
278
  export default generateImageHandler;
@@ -31,13 +31,15 @@ const handlePostRequest = async (req, res) => {
31
31
  });
32
32
  return;
33
33
  }
34
+ Object.assign(transports[sessionId].context, req.unchainedContext);
34
35
  transport = transports[sessionId].transport;
35
36
  }
36
37
  else if (!sessionId && isInitializeRequest(req.body)) {
38
+ const contextHolder = req.unchainedContext;
37
39
  transport = new StreamableHTTPServerTransport({
38
40
  sessionIdGenerator: () => crypto.randomUUID(),
39
41
  onsessioninitialized: (sessionId) => {
40
- transports[sessionId] = { transport, userId: currentUserId };
42
+ transports[sessionId] = { transport, userId: currentUserId, context: contextHolder };
41
43
  },
42
44
  });
43
45
  transport.onclose = () => {
@@ -45,12 +47,12 @@ const handlePostRequest = async (req, res) => {
45
47
  delete transports[transport.sessionId];
46
48
  }
47
49
  };
48
- const roles = req.unchainedContext.user?.roles || [];
50
+ const roles = contextHolder.user?.roles || [];
49
51
  const { default: initMCPServer } = await import("../mcp/index.js");
50
52
  const server = initMCPServer(new McpServer({
51
53
  name: 'Unchained MCP Server',
52
54
  version: '1.0.0',
53
- }), req.unchainedContext, roles);
55
+ }), contextHolder, roles);
54
56
  await server.connect(transport);
55
57
  }
56
58
  else {
@@ -74,6 +76,7 @@ const handleSessionRequest = async (req, res) => {
74
76
  res.status(400).send('Invalid or missing session ID');
75
77
  return;
76
78
  }
79
+ Object.assign(transports[sessionId].context, req.unchainedContext);
77
80
  const transport = transports[sessionId].transport;
78
81
  await transport.handleRequest(req, res);
79
82
  };
@@ -45,13 +45,15 @@ const mcpHandler = async (req, res) => {
45
45
  id: null,
46
46
  }));
47
47
  }
48
+ Object.assign(transports[sessionId].context, req.unchainedContext);
48
49
  transport = transports[sessionId].transport;
49
50
  }
50
51
  else if (!sessionId && isInitializeRequest(req.body)) {
52
+ const contextHolder = req.unchainedContext;
51
53
  transport = new StreamableHTTPServerTransport({
52
54
  sessionIdGenerator: () => crypto.randomUUID(),
53
55
  onsessioninitialized: (sessionId) => {
54
- transports[sessionId] = { transport, userId: currentUserId };
56
+ transports[sessionId] = { transport, userId: currentUserId, context: contextHolder };
55
57
  },
56
58
  });
57
59
  transport.onclose = () => {
@@ -64,7 +66,7 @@ const mcpHandler = async (req, res) => {
64
66
  const server = initMCPServer(new McpServer({
65
67
  name: 'Unchained MCP Server',
66
68
  version: '1.0.0',
67
- }), req.unchainedContext, roles);
69
+ }), contextHolder, roles);
68
70
  await server.connect(transport);
69
71
  }
70
72
  else {
@@ -85,6 +87,7 @@ const mcpHandler = async (req, res) => {
85
87
  if (!sessionId || !transports[sessionId] || transports[sessionId].userId !== currentUserId) {
86
88
  return res.status(400).send('Invalid or missing session ID');
87
89
  }
90
+ Object.assign(transports[sessionId].context, req.unchainedContext);
88
91
  const transport = transports[sessionId].transport;
89
92
  await transport.handleRequest(req.raw, res.raw);
90
93
  return res;
@@ -1,4 +1,4 @@
1
1
  import { quotationManagement, QuotationManagementSchema } from "./quotationManagement.js";
2
2
  export const registerQuotationTools = (server, context) => {
3
- server.tool('quotation_management', 'Unified quotation management system. Supports: LIST (get quotations with filters and pagination), GET (single quotation by ID), COUNT (count quotations), REQUEST (create new quotation request for a product), VERIFY (verify a REQUESTED quotation), MAKE_PROPOSAL (create proposal for PROCESSING quotation), REJECT (reject any quotation except FULFILLED). Quotations go through lifecycle: REQUESTED → PROCESSING → PROPOSED → FULFILLED/REJECTED.', QuotationManagementSchema, async (params) => quotationManagement(context, params));
3
+ server.tool('quotation_management', 'Unified quotation management system. Supports: LIST (get quotations with filters and pagination), GET (single quotation by ID), COUNT (count quotations), VERIFY (verify a REQUESTED quotation), MAKE_PROPOSAL (create proposal for PROCESSING quotation), REJECT (reject any quotation except FULFILLED). Quotations go through lifecycle: REQUESTED → PROCESSING → PROPOSED → FULFILLED/REJECTED.', QuotationManagementSchema, async (params) => quotationManagement(context, params));
4
4
  };
@@ -1,4 +1,4 @@
1
- import { GraphQLJSON, GraphQLTimestamp, GraphQLDateTimeISO, GraphQLDate, GraphQLLocale, } from 'graphql-scalars';
1
+ import { GraphQLJSON, GraphQLTimestamp, GraphQLDateTimeISO, GraphQLDate, GraphQLLocale, GraphQLPhoneNumber, } from 'graphql-scalars';
2
2
  import Query from "./queries/index.js";
3
3
  import Mutation from "./mutations/index.js";
4
4
  import Types from "./type/index.js";
@@ -13,4 +13,5 @@ export default {
13
13
  Date: GraphQLDate,
14
14
  Timestamp: GraphQLTimestamp,
15
15
  Locale: GraphQLLocale,
16
+ PhoneNumber: GraphQLPhoneNumber,
16
17
  };
@@ -1,6 +1,6 @@
1
1
  import type { Context } from '../../../context.ts';
2
2
  export default function createBookmark(root: never, { productId, userId, meta }: {
3
3
  productId: string;
4
- userId: string;
4
+ userId?: string;
5
5
  meta?: any;
6
6
  }, { modules, userId: currentUserId }: Context): Promise<import("mongodb").WithId<import("@unchainedshop/core-bookmarks").Bookmark> | null>;
@@ -1,7 +1,8 @@
1
1
  import { log } from '@unchainedshop/logger';
2
2
  import { BookmarkAlreadyExistsError, InvalidIdError, ProductNotFoundError } from "../../../errors.js";
3
3
  export default async function createBookmark(root, { productId, userId, meta }, { modules, userId: currentUserId }) {
4
- log(`mutation createBookmark for ${userId}`, {
4
+ const targetUserId = userId || currentUserId;
5
+ log(`mutation createBookmark for ${targetUserId}`, {
5
6
  productId,
6
7
  userId: currentUserId,
7
8
  });
@@ -11,13 +12,13 @@ export default async function createBookmark(root, { productId, userId, meta },
11
12
  throw new ProductNotFoundError({ productId });
12
13
  const [bookmark] = await modules.bookmarks.findBookmarks({
13
14
  productId,
14
- userId,
15
+ userId: targetUserId,
15
16
  meta,
16
17
  });
17
18
  if (bookmark)
18
19
  throw new BookmarkAlreadyExistsError({ bookmarkId: bookmark._id });
19
20
  const bookmarkId = await modules.bookmarks.create({
20
- userId,
21
+ userId: targetUserId,
21
22
  productId,
22
23
  meta,
23
24
  });
@@ -623,6 +623,10 @@ declare const types: {
623
623
  isExpired: (obj: import("@unchainedshop/core-quotations").Quotation, { referenceDate }: {
624
624
  referenceDate: Date;
625
625
  }, { modules }: import("../../context.ts").Context) => boolean;
626
+ price: (obj: import("@unchainedshop/core-quotations").Quotation) => {
627
+ amount: number;
628
+ currencyCode: string;
629
+ } | null;
626
630
  product: (obj: import("@unchainedshop/core-quotations").Quotation, _: never, { loaders }: import("../../context.ts").Context) => Promise<import("@unchainedshop/core-products").Product>;
627
631
  status: (obj: import("@unchainedshop/core-quotations").Quotation, _: never, { modules }: import("../../context.ts").Context) => import("@unchainedshop/core-quotations").QuotationStatus;
628
632
  user: (obj: import("@unchainedshop/core-quotations").Quotation, _: never, { loaders }: import("../../context.ts").Context) => Promise<import("@unchainedshop/core-users").User>;
@@ -6,6 +6,10 @@ export declare const Quotation: {
6
6
  isExpired: (obj: QuotationType, { referenceDate }: {
7
7
  referenceDate: Date;
8
8
  }, { modules }: Context) => boolean;
9
+ price: (obj: QuotationType) => {
10
+ amount: number;
11
+ currencyCode: string;
12
+ } | null;
9
13
  product: (obj: QuotationType, _: never, { loaders }: Context) => Promise<import("@unchainedshop/core-products").Product>;
10
14
  status: (obj: QuotationType, _: never, { modules }: Context) => import("@unchainedshop/core-quotations").QuotationStatus;
11
15
  user: (obj: QuotationType, _: never, { loaders }: Context) => Promise<import("@unchainedshop/core-users").User>;
@@ -2,6 +2,7 @@ export const Quotation = {
2
2
  country: async (obj, _, { loaders }) => obj.countryCode ? loaders.countryLoader.load({ isoCode: obj.countryCode }) : null,
3
3
  currency: async (obj, _, { loaders }) => obj.currencyCode ? loaders.currencyLoader.load({ isoCode: obj.currencyCode }) : null,
4
4
  isExpired: (obj, { referenceDate }, { modules }) => modules.quotations.isExpired(obj, { referenceDate }),
5
+ price: (obj) => obj.price != null && obj.currencyCode ? { amount: obj.price, currencyCode: obj.currencyCode } : null,
5
6
  product: async (obj, _, { loaders }) => {
6
7
  const product = await loaders.productLoader.load({
7
8
  productId: obj.productId,
@@ -171,6 +171,7 @@ export const loggedIn = (role, actions) => {
171
171
  role.allow(actions.reviewProduct, () => true);
172
172
  role.allow(actions.updateProductReview, isOwnedProductReview);
173
173
  role.allow(actions.requestQuotation, () => true);
174
+ role.allow(actions.viewQuotation, isOwnedQuotation);
174
175
  role.allow(actions.answerQuotation, isOwnedQuotation);
175
176
  role.allow(actions.manageBookmarks, isOwnedBookmark);
176
177
  role.allow(actions.createBookmark, isOwnBookmarkUser);
@@ -8,7 +8,7 @@ export default [
8
8
  input UserProfileInput {
9
9
  displayName: String
10
10
  birthday: Timestamp
11
- phoneMobile: String
11
+ phoneMobile: PhoneNumber
12
12
  gender: String
13
13
  address: AddressInput
14
14
  }
@@ -27,7 +27,7 @@ export default [
27
27
 
28
28
  input ContactInput {
29
29
  emailAddress: String
30
- telNumber: String
30
+ telNumber: PhoneNumber
31
31
  }
32
32
 
33
33
  input CreateLanguageInput {
@@ -6,5 +6,6 @@ export default [
6
6
  scalar Timestamp
7
7
  scalar LowerCaseString
8
8
  scalar Locale
9
+ scalar PhoneNumber
9
10
  `,
10
11
  ];
@@ -57,6 +57,11 @@ export default [
57
57
  country: Country
58
58
  currency: Currency
59
59
  configuration: [ProductConfigurationParameter!]
60
+
61
+ """
62
+ Proposed unit price (minor units of the quotation's currency), set when the quotation reaches PROPOSED
63
+ """
64
+ price: Price
60
65
  }
61
66
  `,
62
67
  ];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unchainedshop/api",
3
3
  "description": "GraphQL API layer for the Unchained Engine with Express/Fastify adapters and MCP server",
4
- "version": "4.8.17",
4
+ "version": "4.8.19",
5
5
  "main": "lib/api-index.js",
6
6
  "types": "lib/api-index.d.ts",
7
7
  "type": "module",
@@ -60,13 +60,13 @@
60
60
  },
61
61
  "homepage": "https://github.com/unchainedshop/unchained#readme",
62
62
  "peerDependencies": {
63
- "@ai-sdk/mcp": "^1",
63
+ "@ai-sdk/mcp": "^2.0.29",
64
64
  "@fastify/cookie": ">= 11 < 12",
65
65
  "@fastify/multipart": ">= 9 < 11",
66
66
  "@fastify/session": ">= 11 < 12",
67
- "@fastify/static": ">= 9 < 10",
67
+ "@fastify/static": ">= 10.1.3 < 11",
68
68
  "@modelcontextprotocol/sdk": ">= 1 < 2",
69
- "ai": ">= 6 < 7",
69
+ "ai": ">= 7 < 8",
70
70
  "express": ">= 5 < 6",
71
71
  "express-session": ">= 1.18 < 2",
72
72
  "fastify": ">= 5.2 < 6",
@@ -129,16 +129,16 @@
129
129
  "zod": "^3.25.76 || ^4"
130
130
  },
131
131
  "devDependencies": {
132
- "@ai-sdk/mcp": "^1.0.5",
132
+ "@ai-sdk/mcp": "^2.0.29",
133
133
  "@fastify/cookie": "^11.0.2",
134
134
  "@fastify/multipart": "^10.0.0",
135
135
  "@fastify/session": "^11.1.0",
136
- "@fastify/static": "^9.0.0",
136
+ "@fastify/static": "^10.1.3",
137
137
  "@modelcontextprotocol/sdk": "^1.15.1",
138
138
  "@types/express": "^5.0.3",
139
139
  "@types/express-session": "^1.18.2",
140
- "@types/node": "^25.0.0",
141
- "ai": "^6.0.14",
140
+ "@types/node": "^26.2.0",
141
+ "ai": "^7.0.58",
142
142
  "express": "^5.1.0",
143
143
  "express-session": "^1.18.1",
144
144
  "fastify": "^5.4.0",