@morscherlab/mint-sdk 1.0.61 → 1.0.63

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 (31) hide show
  1. package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
  2. package/dist/analysisArtifactTypes-Cu40dzSG.js.map +1 -1
  3. package/dist/components/index.js +1 -1
  4. package/dist/{components-CpnOaPbP.js → components-DdzHh3a_.js} +2 -2
  5. package/dist/{components-CpnOaPbP.js.map → components-DdzHh3a_.js.map} +1 -1
  6. package/dist/composables/analysisArtifactTarget.d.ts +5 -0
  7. package/dist/composables/index.d.ts +2 -0
  8. package/dist/composables/index.js +3 -3
  9. package/dist/composables/platformContextHelpers.d.ts +2 -0
  10. package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
  11. package/dist/{composables-OYTD0Y3r.js → composables-BUUphBqg.js} +54 -6
  12. package/dist/composables-BUUphBqg.js.map +1 -0
  13. package/dist/index.js +4 -4
  14. package/dist/install.js +1 -1
  15. package/dist/types/analysisArtifactTypes.d.ts +8 -0
  16. package/dist/types/index.d.ts +1 -1
  17. package/dist/{useAnalysisArtifacts-Ob8UtVwl.js → useAnalysisArtifacts-C3l0a0vu.js} +30 -4
  18. package/dist/useAnalysisArtifacts-C3l0a0vu.js.map +1 -0
  19. package/package.json +1 -1
  20. package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
  21. package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
  22. package/src/__tests__/composables/usePluginClient.test.ts +95 -1
  23. package/src/composables/analysisArtifactTarget.ts +53 -0
  24. package/src/composables/index.ts +5 -0
  25. package/src/composables/platformContextHelpers.ts +10 -3
  26. package/src/composables/useAnalysisArtifacts.ts +48 -0
  27. package/src/composables/usePluginClient.ts +21 -5
  28. package/src/types/analysisArtifactTypes.ts +9 -0
  29. package/src/types/index.ts +1 -0
  30. package/dist/composables-OYTD0Y3r.js.map +0 -1
  31. package/dist/useAnalysisArtifacts-Ob8UtVwl.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mint-sdk",
3
- "version": "1.0.61",
3
+ "version": "1.0.63",
4
4
  "description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,77 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import {
4
+ buildAnalysisArtifactTargetQuery,
5
+ parseAnalysisArtifactTarget,
6
+ } from '../../composables/analysisArtifactTarget'
7
+ import {
8
+ currentExperimentIdFromUrl,
9
+ resolveCurrentPluginId,
10
+ } from '../../composables/platformContextHelpers'
11
+
12
+ describe('analysis artifact target', () => {
13
+ afterEach(() => {
14
+ delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
15
+ vi.unstubAllGlobals()
16
+ })
17
+
18
+ it('should resolve the canonical injected plugin id', () => {
19
+ ;(window as unknown as { __MINT_PLATFORM__: unknown }).__MINT_PLATFORM__ = {
20
+ isIntegrated: true,
21
+ plugin: { id: 'LEAF', name: 'Leaf UI' },
22
+ theme: 'system',
23
+ }
24
+
25
+ expect(resolveCurrentPluginId()).toBe('LEAF')
26
+ })
27
+
28
+ it('should tolerate a host that provides a partial location object', () => {
29
+ vi.stubGlobal('window', {
30
+ location: { search: '' },
31
+ })
32
+
33
+ expect(currentExperimentIdFromUrl()).toBeUndefined()
34
+ })
35
+
36
+ it('should build the canonical readable query when an artifact id hint is present', () => {
37
+ const query = buildAnalysisArtifactTargetQuery({
38
+ experimentId: 66,
39
+ artifactPluginId: 'LEAF',
40
+ artifactKey: 'exp316_260703_ys_rna_ide_scr',
41
+ artifactId: 206,
42
+ })
43
+
44
+ expect(query.toString()).toBe(
45
+ 'experiment_id=66&artifact_plugin_id=LEAF'
46
+ + '&artifact_key=exp316_260703_ys_rna_ide_scr&artifact_id=206',
47
+ )
48
+ })
49
+
50
+ it('should parse the canonical identity without requiring an artifact id hint', () => {
51
+ const target = parseAnalysisArtifactTarget(new URLSearchParams(
52
+ 'experiment_id=66&artifact_plugin_id=LEAF&artifact_key=result',
53
+ ))
54
+
55
+ expect(target).toEqual({
56
+ experimentId: 66,
57
+ artifactPluginId: 'LEAF',
58
+ artifactKey: 'result',
59
+ })
60
+ })
61
+
62
+ it('should reject an incomplete target instead of guessing another artifact', () => {
63
+ const target = parseAnalysisArtifactTarget(new URLSearchParams(
64
+ 'experiment_id=66&artifact_key=result',
65
+ ))
66
+
67
+ expect(target).toBeNull()
68
+ })
69
+
70
+ it('should reject non-positive numeric identifiers', () => {
71
+ const target = parseAnalysisArtifactTarget(new URLSearchParams(
72
+ 'experiment_id=0&artifact_plugin_id=LEAF&artifact_key=result&artifact_id=-1',
73
+ ))
74
+
75
+ expect(target).toBeNull()
76
+ })
77
+ })
@@ -19,7 +19,10 @@ vi.mock('../../composables/useApi', () => ({
19
19
  }))
20
20
 
21
21
  import { useAnalysisArtifacts } from '../../composables/useAnalysisArtifacts'
22
- import type { AnalysisArtifactSummary } from '../../types/analysisArtifactTypes'
22
+ import type {
23
+ AnalysisArtifactDetail,
24
+ AnalysisArtifactSummary,
25
+ } from '../../types/analysisArtifactTypes'
23
26
 
24
27
  // ---------------------------------------------------------------------------
25
28
  // Fixtures
@@ -177,6 +180,76 @@ describe('useAnalysisArtifacts', () => {
177
180
  })
178
181
  })
179
182
 
183
+ describe('resolveTarget', () => {
184
+ it('should resolve the exact stable identity when the numeric hint is stale', async () => {
185
+ const detail = {
186
+ ...makeArtifact({
187
+ id: 2,
188
+ experiment_id: 42,
189
+ plugin_id: 'plugin-b',
190
+ artifact_key: 'peaks',
191
+ }),
192
+ result: { schema_version: 'mint.analysis_file.v1' },
193
+ tree: [],
194
+ summary: null,
195
+ } satisfies AnalysisArtifactDetail
196
+ mockGet
197
+ .mockResolvedValueOnce({ artifacts: ARTIFACTS })
198
+ .mockResolvedValueOnce(detail)
199
+ const { resolveTarget } = setup()
200
+
201
+ const resolved = await resolveTarget({
202
+ experimentId: 42,
203
+ artifactPluginId: 'plugin-b',
204
+ artifactKey: 'peaks',
205
+ artifactId: 999,
206
+ })
207
+
208
+ expect({
209
+ resolvedId: resolved?.id,
210
+ detailPath: mockGet.mock.calls[1]?.[0],
211
+ }).toEqual({
212
+ resolvedId: 2,
213
+ detailPath: '/experiments/42/analysis-artifacts/2',
214
+ })
215
+ })
216
+
217
+ it('should fail explicitly when the stable identity is not available', async () => {
218
+ const { resolveTarget, error } = setup()
219
+
220
+ const resolved = await resolveTarget({
221
+ experimentId: 42,
222
+ artifactPluginId: 'LEAF',
223
+ artifactKey: 'missing',
224
+ })
225
+
226
+ expect({ resolved, error: error.value }).toEqual({
227
+ resolved: null,
228
+ error: 'Analysis artifact LEAF/missing is not available.',
229
+ })
230
+ })
231
+
232
+ it('should reject a target from another experiment before making a request', async () => {
233
+ const { resolveTarget, error } = setup()
234
+
235
+ const resolved = await resolveTarget({
236
+ experimentId: 66,
237
+ artifactPluginId: 'LEAF',
238
+ artifactKey: 'result',
239
+ })
240
+
241
+ expect({
242
+ resolved,
243
+ error: error.value,
244
+ requests: mockGet.mock.calls,
245
+ }).toEqual({
246
+ resolved: null,
247
+ error: 'Analysis artifact target belongs to experiment 66, not 42.',
248
+ requests: [],
249
+ })
250
+ })
251
+ })
252
+
180
253
  // -------------------------------------------------------------------------
181
254
  // experiment id resolution
182
255
  // -------------------------------------------------------------------------
@@ -62,8 +62,10 @@ function pendingEventStreamResponse(): {
62
62
  describe('usePluginClient', () => {
63
63
  let requestConfigs: AxiosRequestConfig[] = []
64
64
  let requestBodies: unknown[] = []
65
+ let requestMethods: string[] = []
65
66
  let nextGetError: Error | null = null
66
67
  let nextGetData: unknown
68
+ let nextPostData: unknown
67
69
  let nextPatchData: unknown
68
70
  let nextPutData: unknown
69
71
 
@@ -71,8 +73,10 @@ describe('usePluginClient', () => {
71
73
  setActivePinia(createPinia())
72
74
  requestConfigs = []
73
75
  requestBodies = []
76
+ requestMethods = []
74
77
  nextGetError = null
75
78
  nextGetData = undefined
79
+ nextPostData = undefined
76
80
  nextPatchData = undefined
77
81
  nextPutData = undefined
78
82
  vi.spyOn(axios.Axios.prototype, 'get').mockImplementation(async function (
@@ -80,6 +84,7 @@ describe('usePluginClient', () => {
80
84
  url: string,
81
85
  config?: AxiosRequestConfig,
82
86
  ) {
87
+ requestMethods.push('get')
83
88
  requestConfigs.push({ url, ...config })
84
89
  if (nextGetError) throw nextGetError
85
90
  return { data: nextGetData ?? { called: url } }
@@ -90,9 +95,10 @@ describe('usePluginClient', () => {
90
95
  data?: unknown,
91
96
  config?: AxiosRequestConfig,
92
97
  ) {
98
+ requestMethods.push('post')
93
99
  requestConfigs.push({ url, ...config })
94
100
  requestBodies.push(data)
95
- return { data: { called: url } }
101
+ return { data: nextPostData ?? { called: url } }
96
102
  })
97
103
  vi.spyOn(axios.Axios.prototype, 'patch').mockImplementation(async function (
98
104
  this: unknown,
@@ -100,6 +106,7 @@ describe('usePluginClient', () => {
100
106
  data?: unknown,
101
107
  config?: AxiosRequestConfig,
102
108
  ) {
109
+ requestMethods.push('patch')
103
110
  requestConfigs.push({ url, ...config })
104
111
  requestBodies.push(data)
105
112
  return { data: nextPatchData ?? { called: url } }
@@ -110,6 +117,7 @@ describe('usePluginClient', () => {
110
117
  data?: unknown,
111
118
  config?: AxiosRequestConfig,
112
119
  ) {
120
+ requestMethods.push('put')
113
121
  requestConfigs.push({ url, ...config })
114
122
  requestBodies.push(data)
115
123
  return { data: nextPutData ?? { called: url } }
@@ -119,6 +127,7 @@ describe('usePluginClient', () => {
119
127
  url: string,
120
128
  config?: AxiosRequestConfig,
121
129
  ) {
130
+ requestMethods.push('delete')
122
131
  requestConfigs.push({ url, ...config })
123
132
  requestBodies.push(config?.data)
124
133
  return { data: { called: url } }
@@ -410,6 +419,91 @@ describe('usePluginClient', () => {
410
419
  expect(requestConfigs[0]!.responseType).toBe('blob')
411
420
  })
412
421
 
422
+ it('downloads generated POST endpoint blobs with the contract body and request options', async () => {
423
+ const expectedBlob = new Blob(['zip'])
424
+ const controller = new AbortController()
425
+ nextPostData = expectedBlob
426
+
427
+ const blob = await downloadPluginEndpoint(
428
+ contract,
429
+ {
430
+ method: 'post',
431
+ path: '/export/{sessionId}/results',
432
+ pathParams: ['sessionId'],
433
+ queryParams: [
434
+ { name: 'revision', fieldName: 'revision', type: 'number', required: true },
435
+ ],
436
+ hasBody: true,
437
+ requestContentType: 'application/json',
438
+ },
439
+ {
440
+ sessionId: 'session-1',
441
+ revision: 4,
442
+ body: { format: 'wide', correction: false },
443
+ },
444
+ { signal: controller.signal },
445
+ )
446
+
447
+ expect({
448
+ blob,
449
+ body: requestBodies[0],
450
+ url: requestConfigs[0]!.url,
451
+ params: requestConfigs[0]!.params,
452
+ signal: requestConfigs[0]!.signal,
453
+ responseType: requestConfigs[0]!.responseType,
454
+ method: requestMethods[0],
455
+ }).toEqual({
456
+ blob: expectedBlob,
457
+ body: { format: 'wide', correction: false },
458
+ url: '/export/session-1/results',
459
+ params: { revision: 4 },
460
+ signal: controller.signal,
461
+ responseType: 'blob',
462
+ method: 'post',
463
+ })
464
+ })
465
+
466
+ it.each(['put', 'patch', 'delete'] as const)(
467
+ 'downloads generated %s endpoint blobs with the contract body',
468
+ async (method) => {
469
+ await downloadPluginEndpoint(
470
+ contract,
471
+ {
472
+ method,
473
+ path: '/exports/{exportId}',
474
+ pathParams: ['exportId'],
475
+ hasBody: true,
476
+ },
477
+ { exportId: 'export-1', body: { format: 'wide' } },
478
+ )
479
+
480
+ expect({
481
+ body: requestBodies[0],
482
+ method: requestMethods[0],
483
+ responseType: requestConfigs[0]!.responseType,
484
+ url: requestConfigs[0]!.url,
485
+ }).toEqual({
486
+ body: { format: 'wide' },
487
+ method,
488
+ responseType: 'blob',
489
+ url: '/exports/export-1',
490
+ })
491
+ },
492
+ )
493
+
494
+ it('rejects multipart download request bodies', async () => {
495
+ await expect(downloadPluginEndpoint(
496
+ contract,
497
+ {
498
+ method: 'post',
499
+ path: '/exports',
500
+ hasBody: true,
501
+ requestContentType: 'multipart/form-data',
502
+ },
503
+ { body: { format: 'wide' } },
504
+ )).rejects.toThrow('Plugin download endpoints do not support multipart request bodies')
505
+ })
506
+
413
507
  it.each([
414
508
  new Blob(['chunk']),
415
509
  new Uint8Array([1, 2, 3]),
@@ -0,0 +1,53 @@
1
+ import type { AnalysisArtifactTarget } from '../types/analysisArtifactTypes'
2
+
3
+ const EXPERIMENT_ID = 'experiment_id'
4
+ const ARTIFACT_PLUGIN_ID = 'artifact_plugin_id'
5
+ const ARTIFACT_KEY = 'artifact_key'
6
+ const ARTIFACT_ID = 'artifact_id'
7
+
8
+ function parsePositiveInteger(value: string | null): number | undefined {
9
+ if (value === null || !/^\d+$/.test(value)) return undefined
10
+ const parsed = Number(value)
11
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined
12
+ }
13
+
14
+ /** Build the canonical query used to open one analysis artifact in a plugin. */
15
+ export function buildAnalysisArtifactTargetQuery(
16
+ target: AnalysisArtifactTarget,
17
+ ): URLSearchParams {
18
+ const query = new URLSearchParams()
19
+ query.set(EXPERIMENT_ID, String(target.experimentId))
20
+ query.set(ARTIFACT_PLUGIN_ID, target.artifactPluginId)
21
+ query.set(ARTIFACT_KEY, target.artifactKey)
22
+ if (target.artifactId !== undefined) {
23
+ query.set(ARTIFACT_ID, String(target.artifactId))
24
+ }
25
+ return query
26
+ }
27
+
28
+ /** Parse a complete canonical target. Incomplete or invalid input is not guessed. */
29
+ export function parseAnalysisArtifactTarget(
30
+ query: URLSearchParams,
31
+ ): AnalysisArtifactTarget | null {
32
+ const experimentId = parsePositiveInteger(query.get(EXPERIMENT_ID))
33
+ const artifactPluginId = query.get(ARTIFACT_PLUGIN_ID)?.trim()
34
+ const artifactKey = query.get(ARTIFACT_KEY)?.trim()
35
+ const rawArtifactId = query.get(ARTIFACT_ID)
36
+ const artifactId = parsePositiveInteger(rawArtifactId)
37
+
38
+ if (
39
+ experimentId === undefined
40
+ || !artifactPluginId
41
+ || !artifactKey
42
+ || (rawArtifactId !== null && artifactId === undefined)
43
+ ) {
44
+ return null
45
+ }
46
+
47
+ return {
48
+ experimentId,
49
+ artifactPluginId,
50
+ artifactKey,
51
+ ...(artifactId === undefined ? {} : { artifactId }),
52
+ }
53
+ }
@@ -147,6 +147,11 @@ export {
147
147
  type UseAnalysisArtifactsOptions,
148
148
  type UseAnalysisArtifactsReturn,
149
149
  } from './useAnalysisArtifacts'
150
+ export {
151
+ buildAnalysisArtifactTargetQuery,
152
+ parseAnalysisArtifactTarget,
153
+ } from './analysisArtifactTarget'
154
+ export { resolveCurrentPluginId } from './platformContextHelpers'
150
155
  export {
151
156
  formatExperimentDate,
152
157
  formatExperimentStatus,
@@ -12,6 +12,12 @@ export function getInjectedPlatformContext(): PlatformContext | undefined {
12
12
  return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__
13
13
  }
14
14
 
15
+ /** Return the platform's canonical plugin identity for the mounted frontend. */
16
+ export function resolveCurrentPluginId(): string | undefined {
17
+ const pluginId = getInjectedPlatformContext()?.plugin?.id
18
+ return typeof pluginId === 'string' && pluginId ? pluginId : undefined
19
+ }
20
+
15
21
  export function getInjectedExperimentContext(): ExperimentAwarePlatformContext | undefined {
16
22
  return getInjectedPlatformContext() as ExperimentAwarePlatformContext | undefined
17
23
  }
@@ -38,10 +44,11 @@ export function currentExperimentIdFromContext(
38
44
 
39
45
  export function currentExperimentIdFromUrl(): number | undefined {
40
46
  if (typeof window === 'undefined') return undefined
41
- const params = new URLSearchParams(window.location.search)
47
+ const location = window.location
48
+ const params = new URLSearchParams(location?.search ?? '')
42
49
  return parseExperimentId(params.get('experimentId') ?? params.get('experiment_id'))
43
- ?? experimentIdFromPath(window.location.pathname)
44
- ?? experimentIdFromPath(window.location.hash.replace(/^#\/?/, '/'))
50
+ ?? experimentIdFromPath(location?.pathname ?? '')
51
+ ?? experimentIdFromPath((location?.hash ?? '').replace(/^#\/?/, '/'))
45
52
  }
46
53
 
47
54
  export function resolveCurrentExperimentId(
@@ -19,6 +19,7 @@ import type {
19
19
  AnalysisArtifactMetadataUpdate,
20
20
  AnalysisArtifactStatusFilter,
21
21
  AnalysisArtifactSummary,
22
+ AnalysisArtifactTarget,
22
23
  } from '../types/analysisArtifactTypes'
23
24
 
24
25
  export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from '../types/analysisArtifactTypes'
@@ -67,6 +68,8 @@ export interface UseAnalysisArtifactsReturn {
67
68
  refresh: () => Promise<AnalysisArtifactSummary[]>
68
69
  /** Fetch one artifact with its result payload; null when it fails or is superseded. */
69
70
  getDetail: (artifactId: number) => Promise<AnalysisArtifactDetail | null>
71
+ /** Resolve and fetch the exact stable artifact identity. */
72
+ resolveTarget: (target: AnalysisArtifactTarget) => Promise<AnalysisArtifactDetail | null>
70
73
  /** Update display_name and/or note; resolves to the updated summary or null. */
71
74
  updateMetadata: (
72
75
  artifactId: number,
@@ -206,6 +209,50 @@ export function useAnalysisArtifacts(
206
209
  }
207
210
  }
208
211
 
212
+ async function resolveTarget(
213
+ target: AnalysisArtifactTarget,
214
+ ): Promise<AnalysisArtifactDetail | null> {
215
+ const experimentId = currentExperimentId.value
216
+ if (experimentId !== target.experimentId) {
217
+ request.setError(
218
+ `Analysis artifact target belongs to experiment ${target.experimentId}, not `
219
+ + `${experimentId ?? 'the current selection'}.`,
220
+ )
221
+ return null
222
+ }
223
+
224
+ const available = await list()
225
+ const summary = available.find((artifact) => (
226
+ artifact.plugin_id === target.artifactPluginId
227
+ && artifact.artifact_key === target.artifactKey
228
+ && artifact.status === 'active'
229
+ ))
230
+ if (!summary) {
231
+ request.setError(
232
+ `Analysis artifact ${target.artifactPluginId}/${target.artifactKey} is not available.`,
233
+ )
234
+ return null
235
+ }
236
+
237
+ const detail = await getDetail(summary.id)
238
+ if (
239
+ detail
240
+ && (
241
+ detail.experiment_id !== target.experimentId
242
+ || detail.plugin_id !== target.artifactPluginId
243
+ || detail.artifact_key !== target.artifactKey
244
+ )
245
+ ) {
246
+ selectedDetail.value = null
247
+ request.setError(
248
+ `Analysis artifact ${target.artifactPluginId}/${target.artifactKey} did not `
249
+ + 'match the requested identity.',
250
+ )
251
+ return null
252
+ }
253
+ return detail
254
+ }
255
+
209
256
  async function mutate(
210
257
  action: string,
211
258
  operation: (experimentId: number) => Promise<AnalysisArtifactSummary>,
@@ -296,6 +343,7 @@ export function useAnalysisArtifacts(
296
343
  list,
297
344
  refresh: list,
298
345
  getDetail,
346
+ resolveTarget,
299
347
  updateMetadata,
300
348
  archive,
301
349
  restore,
@@ -486,13 +486,29 @@ export async function downloadPluginEndpoint(
486
486
  payload?: unknown,
487
487
  options: DownloadPluginEndpointOptions = {},
488
488
  ): Promise<Blob> {
489
+ if (endpoint.requestContentType === MULTIPART_FORM_DATA) {
490
+ throw new Error('[MINT SDK] Plugin download endpoints do not support multipart request bodies')
491
+ }
492
+
489
493
  const parts = pluginEndpointRequestParts(contract, endpoint, payload, options.baseUrl)
490
494
  const api = useApi({ baseUrl: parts.baseUrl })
491
- const blob = await api.get<Blob>(parts.path, {
492
- ...(queryConfig(parts.query) ?? {}),
493
- ...(options.signal ? { signal: options.signal } : {}),
494
- responseType: 'blob',
495
- })
495
+ const config = {
496
+ ...(endpointRequestConfig(endpoint, parts.query, { signal: options.signal }) ?? {}),
497
+ responseType: 'blob' as const,
498
+ }
499
+ const rawBody = requestBodyFromPayload(payload, parts.requestPayload, endpoint)
500
+ const body = rawBody === undefined ? undefined : endpointRequestBody(endpoint, rawBody)
501
+ let blob: Blob
502
+
503
+ if (endpoint.method === 'get') blob = await api.get<Blob>(parts.path, config)
504
+ else if (endpoint.method === 'post') blob = await api.post<Blob>(parts.path, body, config)
505
+ else if (endpoint.method === 'put') blob = await api.put<Blob>(parts.path, body, config)
506
+ else if (endpoint.method === 'patch') blob = await api.patch<Blob>(parts.path, body, config)
507
+ else if (endpoint.method === 'delete') {
508
+ blob = await api.delete<Blob>(parts.path, body === undefined ? config : { ...config, data: body })
509
+ } else {
510
+ throw new Error(`[MINT SDK] Unsupported plugin endpoint method: ${endpoint.method}`)
511
+ }
496
512
 
497
513
  if (options.filename) {
498
514
  downloadBlob(blob, options.filename, {
@@ -38,6 +38,15 @@ export interface AnalysisArtifactListResponse {
38
38
  artifacts: AnalysisArtifactSummary[]
39
39
  }
40
40
 
41
+ /** Stable identity used when one plugin opens another plugin's artifact. */
42
+ export interface AnalysisArtifactTarget {
43
+ experimentId: number
44
+ artifactPluginId: string
45
+ artifactKey: string
46
+ /** Optional lookup hint. The stable identity remains the three fields above. */
47
+ artifactId?: number
48
+ }
49
+
41
50
  /** Body of PATCH /experiments/{id}/analysis-artifacts/{artifactId}. */
42
51
  export interface AnalysisArtifactMetadataUpdate {
43
52
  display_name?: string
@@ -182,6 +182,7 @@ export type {
182
182
  AnalysisArtifactSummary,
183
183
  AnalysisArtifactDetail,
184
184
  AnalysisArtifactListResponse,
185
+ AnalysisArtifactTarget,
185
186
  AnalysisArtifactMetadataUpdate,
186
187
  ArtifactSaveMode,
187
188
  ArtifactFileSource,