@sequent-org/moodboard 1.4.6 → 1.4.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -1,3 +1,5 @@
1
+ import { isV2ImageDownloadUrl } from '../services/AssetUrlPolicy.js';
2
+
1
3
  // src/core/ApiClient.js
2
4
  export class ApiClient {
3
5
  constructor(baseUrl, authToken = null) {
@@ -159,6 +161,12 @@ export class ApiClient {
159
161
  if (hasForbiddenInlineSrc) {
160
162
  throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
161
163
  }
164
+ if (topSrc && !isV2ImageDownloadUrl(topSrc)) {
165
+ throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 src URL. Save is blocked.`);
166
+ }
167
+ if (propSrc && !isV2ImageDownloadUrl(propSrc)) {
168
+ throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 properties.src URL. Save is blocked.`);
169
+ }
162
170
 
163
171
  const cleanedObj = { ...obj };
164
172
 
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { Events } from './events/Events.js';
5
5
  import { logMindmapCompoundDebug } from '../mindmap/MindmapCompoundContract.js';
6
+ import { isV2ImageDownloadUrl } from '../services/AssetUrlPolicy.js';
6
7
  export class SaveManager {
7
8
  constructor(eventBus, options = {}) {
8
9
  this.eventBus = eventBus;
@@ -245,6 +246,12 @@ export class SaveManager {
245
246
  if (/^data:/i.test(topSrc) || /^blob:/i.test(topSrc) || /^data:/i.test(propSrc) || /^blob:/i.test(propSrc)) {
246
247
  throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
247
248
  }
249
+ if (topSrc && !isV2ImageDownloadUrl(topSrc)) {
250
+ throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 src URL. Save is blocked.`);
251
+ }
252
+ if (propSrc && !isV2ImageDownloadUrl(propSrc)) {
253
+ throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 properties.src URL. Save is blocked.`);
254
+ }
248
255
  }
249
256
  }
250
257
 
@@ -1,6 +1,7 @@
1
1
  import { Events } from '../events/Events.js';
2
2
  import { PasteObjectCommand } from '../commands/index.js';
3
3
  import { RevitScreenshotMetadataService } from '../../services/RevitScreenshotMetadataService.js';
4
+ import { isV2ImageDownloadUrl } from '../../services/AssetUrlPolicy.js';
4
5
 
5
6
  export function setupClipboardFlow(core) {
6
7
  const revitMetadataService = new RevitScreenshotMetadataService(console);
@@ -21,7 +22,12 @@ export function setupClipboardFlow(core) {
21
22
 
22
23
  const ensureServerImage = async ({ src, name, imageId }) => {
23
24
  if (imageId) {
24
- return { src, name, imageId };
25
+ const serverUrl = typeof src === 'string' ? src.trim() : '';
26
+ if (!isV2ImageDownloadUrl(serverUrl)) {
27
+ alert('Некорректный адрес изображения. Изображение не добавлено.');
28
+ return null;
29
+ }
30
+ return { src: serverUrl, name, imageId };
25
31
  }
26
32
  if (!core.imageUploadService) {
27
33
  alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
@@ -39,8 +45,12 @@ export function setupClipboardFlow(core) {
39
45
  const blob = await response.blob();
40
46
  uploadResult = await core.imageUploadService.uploadImage(blob, name || 'clipboard-image');
41
47
  }
48
+ const serverUrl = typeof uploadResult.url === 'string' ? uploadResult.url.trim() : '';
49
+ if (!isV2ImageDownloadUrl(serverUrl)) {
50
+ throw new Error('Сервер вернул некорректный URL изображения');
51
+ }
42
52
  return {
43
- src: uploadResult.url,
53
+ src: serverUrl,
44
54
  name: uploadResult.name || name,
45
55
  imageId: uploadResult.imageId || uploadResult.id
46
56
  };
@@ -272,10 +282,12 @@ export function setupClipboardFlow(core) {
272
282
  const img = new Image();
273
283
  img.decoding = 'async';
274
284
  img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
275
- img.onerror = () => { void placeWithAspect(0, 0); };
285
+ img.onerror = () => {
286
+ alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
287
+ };
276
288
  img.src = uploaded.src;
277
289
  } catch (_) {
278
- void placeWithAspect(0, 0);
290
+ alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
279
291
  }
280
292
  });
281
293
 
@@ -320,10 +332,12 @@ export function setupClipboardFlow(core) {
320
332
  const img = new Image();
321
333
  img.decoding = 'async';
322
334
  img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
323
- img.onerror = () => { void placeWithAspect(0, 0); };
335
+ img.onerror = () => {
336
+ alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
337
+ };
324
338
  img.src = uploaded.src;
325
339
  } catch (_) {
326
- void placeWithAspect(0, 0);
340
+ alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
327
341
  }
328
342
  });
329
343
 
@@ -25,8 +25,6 @@ export function setupSaveFlow(core) {
25
25
  core.eventBus.on(Events.Save.Success, async () => {
26
26
  if (typeof core.revealPendingObjectsAfterSave === 'function') {
27
27
  core.revealPendingObjectsAfterSave();
28
- } else if (typeof core.revealPendingImageObjectsAfterSave === 'function') {
29
- core.revealPendingImageObjectsAfterSave();
30
28
  }
31
29
  // ВРЕМЕННО ОТКЛЮЧЕНО:
32
30
  // cleanup-фича требует доработки контракта и серверной поддержки.
package/src/core/index.js CHANGED
@@ -465,11 +465,6 @@ export class CoreMoodBoard {
465
465
  this._pendingPersistAckVisibilityIds.clear();
466
466
  }
467
467
 
468
- // Backward-compat alias for tests/integrations created in previous step.
469
- revealPendingImageObjectsAfterSave() {
470
- this.revealPendingObjectsAfterSave();
471
- }
472
-
473
468
  // === Прикрепления к фреймам ===
474
469
  // Логика фреймов перенесена в FrameService
475
470
 
@@ -21,12 +21,10 @@ function resolveMoodboardApiBase(board) {
21
21
  const raw = String(board?.options?.apiUrl || '').trim();
22
22
  if (!raw) return '/api/v2/moodboard';
23
23
 
24
- // Совместимость с legacy конфигом: /api/moodboard -> /api/v2/moodboard
25
- if (raw.endsWith('/api/moodboard')) {
26
- return raw.replace(/\/api\/moodboard$/, '/api/v2/moodboard');
27
- }
28
- if (raw.endsWith('/api/moodboard/')) {
29
- return raw.replace(/\/api\/moodboard\/$/, '/api/v2/moodboard/');
24
+ const hasLegacyPath = /\/api\/moodboard(?:\/|$)/i.test(raw);
25
+ const hasV2Path = /\/api\/v2\/moodboard(?:\/|$)/i.test(raw);
26
+ if (hasLegacyPath && !hasV2Path) {
27
+ throw new Error('Legacy apiUrl "/api/moodboard" is not supported. Use "/api/v2/moodboard".');
30
28
  }
31
29
 
32
30
  return raw;
@@ -0,0 +1,26 @@
1
+ export function isV2ImageDownloadUrl(url) {
2
+ if (typeof url !== 'string') return false;
3
+ const raw = url.trim();
4
+ if (!raw) return false;
5
+ if (/^\/api\/v2\/images\/[^/]+\/download$/i.test(raw)) return true;
6
+ try {
7
+ const parsed = new URL(raw);
8
+ return /^\/api\/v2\/images\/[^/]+\/download$/i.test(parsed.pathname);
9
+ } catch (_) {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ export function isV2FileDownloadUrl(url) {
15
+ if (typeof url !== 'string') return false;
16
+ const raw = url.trim();
17
+ if (!raw) return false;
18
+ if (/^\/api\/v2\/files\/[^/]+\/download$/i.test(raw)) return true;
19
+ try {
20
+ const parsed = new URL(raw);
21
+ return /^\/api\/v2\/files\/[^/]+\/download$/i.test(parsed.pathname);
22
+ } catch (_) {
23
+ return false;
24
+ }
25
+ }
26
+
@@ -1,3 +1,5 @@
1
+ import { isV2FileDownloadUrl } from './AssetUrlPolicy.js';
2
+
1
3
  /**
2
4
  * Сервис для загрузки и управления файлами на сервере
3
5
  */
@@ -92,10 +94,22 @@ export class FileUploadService {
92
94
  throw new Error(result.message || 'Ошибка загрузки файла');
93
95
  }
94
96
 
97
+ const fileId = result.data.fileId || result.data.id;
98
+ const serverUrl = typeof result.data.url === 'string' ? result.data.url.trim() : '';
99
+ if (!fileId) {
100
+ throw new Error('Сервер не вернул fileId.');
101
+ }
102
+ if (!isV2FileDownloadUrl(serverUrl)) {
103
+ throw new Error('Некорректный URL файла от сервера. Ожидается /api/v2/files/{fileId}/download');
104
+ }
105
+ if (!this._matchesFileIdInUrl(serverUrl, fileId)) {
106
+ throw new Error('fileId не совпадает с URL файла от сервера.');
107
+ }
108
+
95
109
  return {
96
- id: result.data.fileId || result.data.id, // Используем fileId как основное поле, id для обратной совместимости
97
- fileId: result.data.fileId || result.data.id, // Добавляем fileId для явного доступа
98
- url: result.data.url,
110
+ id: fileId, // Используем fileId как основное поле, id для обратной совместимости
111
+ fileId, // Добавляем fileId для явного доступа
112
+ url: serverUrl,
99
113
  size: result.data.size,
100
114
  name: result.data.name,
101
115
  type: result.data.type
@@ -107,6 +121,23 @@ export class FileUploadService {
107
121
  }
108
122
  }
109
123
 
124
+ _matchesFileIdInUrl(url, fileId) {
125
+ const id = typeof fileId === 'string' ? fileId.trim() : '';
126
+ if (!id || typeof url !== 'string') return false;
127
+ const raw = url.trim();
128
+ const relativeMatch = raw.match(/^\/api\/v2\/files\/([^/]+)\/download$/i);
129
+ if (relativeMatch) {
130
+ return decodeURIComponent(relativeMatch[1]) === id;
131
+ }
132
+ try {
133
+ const parsed = new URL(raw);
134
+ const absoluteMatch = parsed.pathname.match(/^\/api\/v2\/files\/([^/]+)\/download$/i);
135
+ return !!absoluteMatch && decodeURIComponent(absoluteMatch[1]) === id;
136
+ } catch (_) {
137
+ return false;
138
+ }
139
+ }
140
+
110
141
  /**
111
142
  * Обновляет метаданные файла на сервере
112
143
  * @param {string} fileId - ID файла
@@ -1,3 +1,5 @@
1
+ import { isV2ImageDownloadUrl } from './AssetUrlPolicy.js';
2
+
1
3
  /**
2
4
  * Сервис для загрузки и управления изображениями на сервере
3
5
  */
@@ -97,10 +99,22 @@ export class ImageUploadService {
97
99
  throw new Error(result.message || 'Ошибка загрузки изображения');
98
100
  }
99
101
 
102
+ const imageId = result.data.imageId || result.data.id;
103
+ const serverUrl = typeof result.data.url === 'string' ? result.data.url.trim() : '';
104
+ if (!imageId) {
105
+ throw new Error('Сервер не вернул imageId.');
106
+ }
107
+ if (!isV2ImageDownloadUrl(serverUrl)) {
108
+ throw new Error('Некорректный URL изображения от сервера. Ожидается /api/v2/images/{imageId}/download');
109
+ }
110
+ if (!this._matchesImageIdInUrl(serverUrl, imageId)) {
111
+ throw new Error('imageId не совпадает с URL изображения от сервера.');
112
+ }
113
+
100
114
  return {
101
- id: result.data.imageId || result.data.id, // Используем imageId как основное поле, id для обратной совместимости
102
- imageId: result.data.imageId || result.data.id, // Добавляем imageId для явного доступа
103
- url: result.data.url,
115
+ id: imageId, // Используем imageId как основное поле, id для обратной совместимости
116
+ imageId, // Добавляем imageId для явного доступа
117
+ url: serverUrl,
104
118
  width: result.data.width,
105
119
  height: result.data.height,
106
120
  name: result.data.name,
@@ -113,6 +127,23 @@ export class ImageUploadService {
113
127
  }
114
128
  }
115
129
 
130
+ _matchesImageIdInUrl(url, imageId) {
131
+ const id = typeof imageId === 'string' ? imageId.trim() : '';
132
+ if (!id || typeof url !== 'string') return false;
133
+ const raw = url.trim();
134
+ const relativeMatch = raw.match(/^\/api\/v2\/images\/([^/]+)\/download$/i);
135
+ if (relativeMatch) {
136
+ return decodeURIComponent(relativeMatch[1]) === id;
137
+ }
138
+ try {
139
+ const parsed = new URL(raw);
140
+ const absoluteMatch = parsed.pathname.match(/^\/api\/v2\/images\/([^/]+)\/download$/i);
141
+ return !!absoluteMatch && decodeURIComponent(absoluteMatch[1]) === id;
142
+ } catch (_) {
143
+ return false;
144
+ }
145
+ }
146
+
116
147
  /**
117
148
  * Загружает изображение из base64 DataURL
118
149
  * @param {string} dataUrl - base64 DataURL