@stackshift-cloud/sdk 1.0.0 → 1.1.0

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,55 @@
1
+ import type { StackShift } from './client.js';
2
+ import type { AssetBucket } from './types.js';
3
+ import type { AssetConnection, AssetConnectionInput, AssetConnectionListResult, AssetMigration, AssetMigrationInput, AssetMigrationListResult, AssetMigrationReport, AssetRelocationInput, AssetS2Bucket, AssetS2BucketListResult, AssetStorageConfiguration, AssetTextExtraction, AssetTextExtractionInput, AssetUploadCapabilityInput, CreatedAssetUploadCapability } from './assets-platform-types.js';
4
+ export declare class AssetConnectionsClient {
5
+ private readonly client;
6
+ constructor(client: StackShift);
7
+ list(): Promise<AssetConnectionListResult>;
8
+ get(connectionId: string): Promise<AssetConnection>;
9
+ create(input: AssetConnectionInput): Promise<AssetConnection>;
10
+ update(connectionId: string, input: Partial<AssetConnectionInput>): Promise<AssetConnection>;
11
+ verify(connectionId: string): Promise<AssetConnection>;
12
+ delete(connectionId: string): Promise<{
13
+ deleted: boolean;
14
+ }>;
15
+ }
16
+ export declare class AssetStorageClient {
17
+ private readonly client;
18
+ constructor(client: StackShift);
19
+ configure(bucketId: string, revision: number, input: AssetStorageConfiguration): Promise<AssetBucket>;
20
+ listS2Buckets(): Promise<AssetS2BucketListResult>;
21
+ createS2Bucket(input: {
22
+ name: string;
23
+ region: string;
24
+ }): Promise<AssetS2Bucket>;
25
+ relocate(bucketId: string, input?: AssetRelocationInput): Promise<AssetMigration>;
26
+ }
27
+ export declare class AssetMigrationsClient {
28
+ private readonly client;
29
+ constructor(client: StackShift);
30
+ start(input: AssetMigrationInput): Promise<AssetMigration>;
31
+ list(options?: {
32
+ limit?: number;
33
+ }): Promise<AssetMigrationListResult>;
34
+ get(migrationId: string): Promise<AssetMigration>;
35
+ cancel(migrationId: string): Promise<{
36
+ canceled: boolean;
37
+ }>;
38
+ retryItem(migrationId: string, itemId: string): Promise<{
39
+ queued: boolean;
40
+ }>;
41
+ report(migrationId: string, format?: 'json'): Promise<AssetMigrationReport>;
42
+ downloadReport(migrationId: string, format?: 'json' | 'csv'): Promise<Blob>;
43
+ }
44
+ export declare class AssetTextClient {
45
+ private readonly client;
46
+ constructor(client: StackShift);
47
+ start(assetId: string, input?: AssetTextExtractionInput): Promise<AssetTextExtraction>;
48
+ get(assetId: string): Promise<AssetTextExtraction>;
49
+ retry(extractionId: string): Promise<AssetTextExtraction>;
50
+ }
51
+ export declare class AssetUploadCapabilitiesClient {
52
+ private readonly client;
53
+ constructor(client: StackShift);
54
+ create(input: AssetUploadCapabilityInput): Promise<CreatedAssetUploadCapability>;
55
+ }
@@ -0,0 +1,90 @@
1
+ const encoded = encodeURIComponent;
2
+ export class AssetConnectionsClient {
3
+ client;
4
+ constructor(client) {
5
+ this.client = client;
6
+ }
7
+ list() { return this.client.request('/assets/connections'); }
8
+ get(connectionId) { return this.client.request(`/assets/connections/${encoded(connectionId)}`); }
9
+ create(input) {
10
+ return this.client.request('/assets/connections', { method: 'POST', body: input });
11
+ }
12
+ update(connectionId, input) {
13
+ return this.client.request(`/assets/connections/${encoded(connectionId)}`, { method: 'PATCH', body: input });
14
+ }
15
+ verify(connectionId) {
16
+ return this.client.request(`/assets/connections/${encoded(connectionId)}/verify`, { method: 'POST' });
17
+ }
18
+ delete(connectionId) {
19
+ return this.client.request(`/assets/connections/${encoded(connectionId)}`, { method: 'DELETE' });
20
+ }
21
+ }
22
+ export class AssetStorageClient {
23
+ client;
24
+ constructor(client) {
25
+ this.client = client;
26
+ }
27
+ configure(bucketId, revision, input) {
28
+ return this.client.request(`/assets/buckets/${encoded(bucketId)}/storage`, {
29
+ method: 'PUT', body: input, headers: { 'If-Match': `"${revision}"` },
30
+ });
31
+ }
32
+ listS2Buckets() { return this.client.request('/assets/s2-buckets'); }
33
+ createS2Bucket(input) {
34
+ return this.client.request('/assets/s2-buckets', { method: 'POST', body: input });
35
+ }
36
+ relocate(bucketId, input = {}) {
37
+ return this.client.request(`/assets/buckets/${encoded(bucketId)}/relocations`, { method: 'POST', body: input });
38
+ }
39
+ }
40
+ export class AssetMigrationsClient {
41
+ client;
42
+ constructor(client) {
43
+ this.client = client;
44
+ }
45
+ start(input) {
46
+ return this.client.request('/assets/migrations', { method: 'POST', body: input });
47
+ }
48
+ list(options = {}) {
49
+ const query = new URLSearchParams();
50
+ if (options.limit !== undefined)
51
+ query.set('limit', String(options.limit));
52
+ const suffix = query.toString();
53
+ return this.client.request(`/assets/migrations${suffix ? `?${suffix}` : ''}`);
54
+ }
55
+ get(migrationId) { return this.client.request(`/assets/migrations/${encoded(migrationId)}`); }
56
+ cancel(migrationId) {
57
+ return this.client.request(`/assets/migrations/${encoded(migrationId)}/cancel`, { method: 'POST' });
58
+ }
59
+ retryItem(migrationId, itemId) {
60
+ return this.client.request(`/assets/migrations/${encoded(migrationId)}/items/${encoded(itemId)}/retry`, { method: 'POST' });
61
+ }
62
+ report(migrationId, format = 'json') {
63
+ return this.client.request(`/assets/migrations/${encoded(migrationId)}/report?format=${format}`);
64
+ }
65
+ async downloadReport(migrationId, format = 'json') {
66
+ return this.client.requestRaw(`/assets/migrations/${encoded(migrationId)}/report?format=${format}`).then((response) => response.blob());
67
+ }
68
+ }
69
+ export class AssetTextClient {
70
+ client;
71
+ constructor(client) {
72
+ this.client = client;
73
+ }
74
+ start(assetId, input = {}) {
75
+ return this.client.request(`/assets/${encoded(assetId)}/text-extraction`, { method: 'POST', body: input });
76
+ }
77
+ get(assetId) { return this.client.request(`/assets/${encoded(assetId)}/text-extraction`); }
78
+ retry(extractionId) {
79
+ return this.client.request(`/assets/text-extractions/${encoded(extractionId)}/retry`, { method: 'POST' });
80
+ }
81
+ }
82
+ export class AssetUploadCapabilitiesClient {
83
+ client;
84
+ constructor(client) {
85
+ this.client = client;
86
+ }
87
+ create(input) {
88
+ return this.client.request('/assets/upload-capabilities', { method: 'POST', body: input });
89
+ }
90
+ }
package/dist/assets.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { StackShift } from './client.js';
2
+ import { AssetConnectionsClient, AssetMigrationsClient, AssetStorageClient, AssetTextClient, AssetUploadCapabilitiesClient } from './assets-platform.js';
3
+ import { AssetWorkflowsClient } from './asset-workflows.js';
4
+ import type { AssetDerivedAsset, AssetImageGenerationInput, AssetTransformDefinition, AssetTransformMaterializeInput } from './asset-media-v2.js';
2
5
  import type { Asset, AssetBucket, AssetBucketInput, AssetAIAccountConfig, AssetPatchInput, AssetCustomDomain, AssetEvent, AssetJob, AssetJobSubmission, AssetLifecycleRule, AssetUsageSummary, AssetWebhook, AssetWebhookDelivery, AssetAIInfo, AssetCollection, AssetContentPolicy, AssetNamedTransformInput, AssetListOptions, AssetListResult, AssetSavedSearch, AssetTranscript, AssetUploadProgressCallbacks, AssetVersion, AssetSignedUploadUrl, AssetSignedUploadUrlOptions, AssetSignedTransformOptions, AssetSignedUrl, AssetSignedUrlOptions, AssetTransformOptions, AssetTransformPreset, AssetUploadOptions } from './types.js';
3
6
  export declare const assetTransformPresets: AssetTransformPreset[];
4
7
  export declare function getAssetTransformPreset(name: string): AssetTransformPreset | undefined;
@@ -6,6 +9,12 @@ export declare function assetTransformOptions(preset: AssetTransformPreset): Ass
6
9
  export declare function assetTransformSpec(options: AssetTransformOptions): string;
7
10
  export declare class AssetsClient {
8
11
  private readonly client;
12
+ readonly connections: AssetConnectionsClient;
13
+ readonly storage: AssetStorageClient;
14
+ readonly migrations: AssetMigrationsClient;
15
+ readonly text: AssetTextClient;
16
+ readonly uploadCapabilities: AssetUploadCapabilitiesClient;
17
+ readonly workflows: AssetWorkflowsClient;
9
18
  constructor(client: StackShift);
10
19
  upload(file: Blob, options?: AssetUploadOptions): Promise<Asset>;
11
20
  list(options?: AssetListOptions): Promise<AssetListResult>;
@@ -95,6 +104,13 @@ export declare class AssetsClient {
95
104
  getPolicy(): Promise<AssetContentPolicy>;
96
105
  updatePolicy(policy: AssetContentPolicy): Promise<AssetContentPolicy>;
97
106
  createTransformation(input: AssetNamedTransformInput): Promise<unknown>;
107
+ createTransformationV2(name: string, definition: AssetTransformDefinition, eager?: boolean): Promise<unknown>;
108
+ materializeTransform(assetId: string, input: AssetTransformMaterializeInput, idempotencyKey?: string): Promise<AssetJobSubmission>;
109
+ derivatives(assetId: string): Promise<{
110
+ derivatives: AssetDerivedAsset[];
111
+ }>;
112
+ generate(input: AssetImageGenerationInput, idempotencyKey: string): Promise<AssetJobSubmission>;
113
+ automation(): Promise<import("./asset-workflows.js").AssetAutomationOverview>;
98
114
  listTransformations(): Promise<unknown>;
99
115
  deleteTransformation(name: string): Promise<{
100
116
  deleted: boolean;
package/dist/assets.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { AssetConnectionsClient, AssetMigrationsClient, AssetStorageClient, AssetTextClient, AssetUploadCapabilitiesClient } from './assets-platform.js';
2
+ import { AssetWorkflowsClient } from './asset-workflows.js';
3
+ import { ChunkedUploadController, putBlob, sha256Blob } from './upload-controller.js';
1
4
  export const assetTransformPresets = [
2
5
  preset('web-thumbnail', 'responsive-web', 'Compact grid thumbnail for libraries and dashboards.', 320, 180, 'fill', 'webp', 78),
3
6
  preset('web-card', 'responsive-web', 'Balanced card image for product and content listings.', 640, 360, 'fill', 'webp', 82),
@@ -41,8 +44,20 @@ export function assetTransformSpec(options) {
41
44
  }
42
45
  export class AssetsClient {
43
46
  client;
47
+ connections;
48
+ storage;
49
+ migrations;
50
+ text;
51
+ uploadCapabilities;
52
+ workflows;
44
53
  constructor(client) {
45
54
  this.client = client;
55
+ this.connections = new AssetConnectionsClient(client);
56
+ this.storage = new AssetStorageClient(client);
57
+ this.migrations = new AssetMigrationsClient(client);
58
+ this.text = new AssetTextClient(client);
59
+ this.uploadCapabilities = new AssetUploadCapabilitiesClient(client);
60
+ this.workflows = new AssetWorkflowsClient(client);
46
61
  }
47
62
  upload(file, options = {}) {
48
63
  return this.client.request('/assets/upload', {
@@ -130,7 +145,13 @@ export class AssetsClient {
130
145
  const target = uploadUrlOrToken.startsWith('http')
131
146
  ? `${uploadUrlOrToken.replace(/\/$/, '')}/parts/${partNumber}`
132
147
  : `${this.originBaseUrl()}/uploads/${encodeURIComponent(uploadUrlOrToken)}/parts/${partNumber}`;
133
- return uploadBlob(target, chunk, callbacks, partNumber, { 'X-Content-SHA256': await sha256Blob(chunk) });
148
+ return putBlob(target, chunk, {
149
+ signal: callbacks.signal,
150
+ onProgress: (progress) => {
151
+ callbacks.onProgress?.(progress);
152
+ callbacks.onPartProgress?.(partNumber, progress);
153
+ },
154
+ }, { 'X-Content-SHA256': await sha256Blob(chunk) });
134
155
  }
135
156
  resumeUploadSession(sessionId) {
136
157
  return this.client.request(`/assets/upload-sessions/${encodeURIComponent(sessionId)}`);
@@ -190,56 +211,22 @@ export class AssetsClient {
190
211
  fileName: options.fileName ?? blobName(file),
191
212
  mimeType: options.mimeType ?? file.type,
192
213
  });
193
- const partSize = session.part_size ?? defaultPartSize;
194
- const sessionId = session.session_id ?? options.uploadSessionId;
195
- if (!sessionId) {
196
- throw new Error('Chunked upload session did not include a resumable session id');
197
- }
198
- const completed = new Map((session.parts ?? []).map((part) => [part.part_number, part.size]));
199
- const loadedByPart = new Map(completed);
200
- const tasks = [];
201
- for (let offset = 0, part = 1; offset < size; offset += partSize, part += 1) {
202
- if (!completed.has(part))
203
- tasks.push({ part, chunk: file.slice(offset, offset + partSize) });
204
- }
205
- const reportProgress = () => {
206
- const loaded = Math.min(size, Array.from(loadedByPart.values()).reduce((sum, value) => sum + value, 0));
207
- callbacks.onProgress?.({ loaded, total: size, percent: size ? Math.round((loaded / size) * 100) : 100 });
208
- };
209
- reportProgress();
210
- const concurrency = boundedInteger(options.concurrency, 4, 1, 8);
211
- const attempts = boundedInteger(options.retryAttempts, 4, 1, 8);
212
- let next = 0;
213
- const worker = async () => {
214
- while (next < tasks.length) {
215
- throwIfAborted(callbacks.signal);
216
- const task = tasks[next++];
217
- await retryWithBackoff(async () => {
218
- throwIfAborted(callbacks.signal);
219
- await this.uploadChunk(session.upload_session_id, task.part, task.chunk, {
220
- signal: callbacks.signal,
221
- onProgress: (progress) => {
222
- loadedByPart.set(task.part, progress.loaded);
223
- callbacks.onPartProgress?.(task.part, progress);
224
- reportProgress();
225
- },
226
- });
227
- loadedByPart.set(task.part, task.chunk.size);
228
- reportProgress();
229
- }, attempts, callbacks.signal);
230
- }
231
- };
232
- try {
233
- await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length || 1) }, worker));
234
- throwIfAborted(callbacks.signal);
235
- return await retryWithBackoff(() => this.completeUploadSession(sessionId), attempts, callbacks.signal);
236
- }
237
- catch (error) {
238
- if (isAbortError(error)) {
239
- await this.cancelUploadSession(sessionId).catch(() => undefined);
240
- }
241
- throw error;
242
- }
214
+ const controller = new ChunkedUploadController({
215
+ create: async () => toControllerSession(session),
216
+ resume: async (sessionId) => toControllerSession(await this.resumeUploadSession(sessionId)),
217
+ uploadPart: async (current, partNumber, chunk, checksum, progress) => {
218
+ const target = `${this.originBaseUrl()}/uploads/${encodeURIComponent(current.uploadToken)}/parts/${partNumber}`;
219
+ await putBlob(target, chunk, progress, { 'X-Content-SHA256': checksum });
220
+ },
221
+ complete: (current) => this.completeUploadSession(current.id),
222
+ cancel: (current) => this.cancelUploadSession(current.id).then(() => undefined),
223
+ });
224
+ return controller.upload(file, {
225
+ key: options.key ?? options.fileName ?? blobName(file) ?? 'upload',
226
+ fileName: options.fileName ?? blobName(file) ?? 'upload',
227
+ mimeType: options.mimeType ?? file.type,
228
+ metadata: options.metadata,
229
+ }, { ...callbacks, concurrency: options.concurrency, retryAttempts: options.retryAttempts, cancelOnAbort: true });
243
230
  }
244
231
  url(assetId, options) {
245
232
  return `${this.client.cdnBaseUrl}/assets/${encodeURIComponent(assetId)}/tr/${encodeURIComponent(transformSpec(options))}`;
@@ -346,6 +333,28 @@ export class AssetsClient {
346
333
  body: input,
347
334
  });
348
335
  }
336
+ createTransformationV2(name, definition, eager = false) {
337
+ return this.client.request('/assets/transformations', {
338
+ method: 'POST', body: { name, definition, eager },
339
+ });
340
+ }
341
+ materializeTransform(assetId, input, idempotencyKey) {
342
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/transforms`, {
343
+ method: 'POST', body: input,
344
+ headers: idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : undefined,
345
+ });
346
+ }
347
+ derivatives(assetId) {
348
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/derivatives`);
349
+ }
350
+ generate(input, idempotencyKey) {
351
+ return this.client.request('/assets/generations', {
352
+ method: 'POST', body: input, headers: { 'Idempotency-Key': idempotencyKey },
353
+ });
354
+ }
355
+ automation() {
356
+ return this.client.request('/assets/automation');
357
+ }
349
358
  listTransformations() {
350
359
  return this.client.request('/assets/transformations');
351
360
  }
@@ -441,7 +450,7 @@ export class AssetsClient {
441
450
  mode: 'single',
442
451
  maxBytes: options.maxBytes ?? file.size,
443
452
  });
444
- await uploadBlob(session.url, file, callbacks);
453
+ await putBlob(session.url, file, callbacks);
445
454
  return session;
446
455
  }
447
456
  }
@@ -495,82 +504,6 @@ const defaultPartSize = 5 * 1024 * 1024;
495
504
  function blobName(file) {
496
505
  return file.name;
497
506
  }
498
- function uploadBlob(url, blob, callbacks, partNumber, headers = {}) {
499
- if (typeof XMLHttpRequest === 'undefined') {
500
- return fetch(url, { method: 'PUT', body: blob, signal: callbacks.signal, headers }).then((response) => {
501
- if (!response.ok)
502
- throw new Error(`Upload failed with ${response.status}`);
503
- const progress = { loaded: blob.size, total: blob.size, percent: 100 };
504
- callbacks.onProgress?.(progress);
505
- if (partNumber)
506
- callbacks.onPartProgress?.(partNumber, progress);
507
- });
508
- }
509
- return new Promise((resolve, reject) => {
510
- const xhr = new XMLHttpRequest();
511
- xhr.open('PUT', url);
512
- for (const [name, value] of Object.entries(headers))
513
- xhr.setRequestHeader(name, value);
514
- const abort = () => xhr.abort();
515
- callbacks.signal?.addEventListener('abort', abort, { once: true });
516
- xhr.upload.onprogress = (event) => {
517
- const total = event.total || blob.size;
518
- const progress = { loaded: event.loaded, total, percent: total ? Math.round((event.loaded / total) * 100) : 0 };
519
- callbacks.onProgress?.(progress);
520
- if (partNumber)
521
- callbacks.onPartProgress?.(partNumber, progress);
522
- };
523
- const cleanup = () => callbacks.signal?.removeEventListener('abort', abort);
524
- xhr.onload = () => {
525
- cleanup();
526
- if (xhr.status >= 200 && xhr.status < 300)
527
- resolve();
528
- else
529
- reject(new Error(`Upload failed with ${xhr.status}`));
530
- };
531
- xhr.onerror = () => { cleanup(); reject(new Error('Upload failed')); };
532
- xhr.onabort = () => { cleanup(); reject(abortError()); };
533
- xhr.send(blob);
534
- });
535
- }
536
- async function sha256Blob(blob) {
537
- if (!globalThis.crypto?.subtle)
538
- throw new Error('Web Crypto SHA-256 is required for multipart uploads');
539
- const digest = await globalThis.crypto.subtle.digest('SHA-256', await blob.arrayBuffer());
540
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
541
- }
542
- function boundedInteger(value, fallback, minimum, maximum) {
543
- if (value === undefined)
544
- return fallback;
545
- if (!Number.isSafeInteger(value))
546
- throw new Error('Upload concurrency and retry attempts must be integers');
547
- return Math.min(maximum, Math.max(minimum, value));
548
- }
549
- async function retryWithBackoff(operation, attempts, signal) {
550
- let lastError;
551
- for (let attempt = 0; attempt < attempts; attempt += 1) {
552
- try {
553
- return await operation();
554
- }
555
- catch (error) {
556
- if (isAbortError(error) || signal?.aborted)
557
- throw abortError();
558
- lastError = error;
559
- if (attempt + 1 < attempts)
560
- await abortableDelay(250 * 2 ** attempt, signal);
561
- }
562
- }
563
- throw lastError;
564
- }
565
- function abortableDelay(milliseconds, signal) {
566
- throwIfAborted(signal);
567
- return new Promise((resolve, reject) => {
568
- const timer = setTimeout(() => { cleanup(); resolve(); }, milliseconds);
569
- const abort = () => { clearTimeout(timer); cleanup(); reject(abortError()); };
570
- const cleanup = () => signal?.removeEventListener('abort', abort);
571
- signal?.addEventListener('abort', abort, { once: true });
572
- });
573
- }
574
507
  function throwIfAborted(signal) {
575
508
  if (signal?.aborted)
576
509
  throw abortError();
@@ -578,6 +511,17 @@ function throwIfAborted(signal) {
578
511
  function abortError() {
579
512
  return typeof DOMException === 'undefined' ? Object.assign(new Error('Upload cancelled'), { name: 'AbortError' }) : new DOMException('Upload cancelled', 'AbortError');
580
513
  }
581
- function isAbortError(error) {
582
- return error instanceof Error && error.name === 'AbortError';
514
+ function toControllerSession(session) {
515
+ const id = session.session_id;
516
+ if (!id)
517
+ throw new Error('Chunked upload session did not include a resumable session id');
518
+ return {
519
+ id,
520
+ uploadToken: session.upload_session_id,
521
+ expiresAt: session.expires_at,
522
+ maxBytes: session.max_bytes,
523
+ partSize: session.part_size ?? defaultPartSize,
524
+ parts: (session.parts ?? []).map((part) => ({ partNumber: part.part_number, size: part.size, checksumSHA256: part.checksum_sha256 })),
525
+ resumeContext: id,
526
+ };
583
527
  }
package/dist/client.d.ts CHANGED
@@ -17,12 +17,18 @@ export declare class StackShift {
17
17
  body?: unknown;
18
18
  headers?: Record<string, string>;
19
19
  }): Promise<T>;
20
+ requestRaw(path: string, init?: {
21
+ method?: string;
22
+ body?: unknown;
23
+ headers?: Record<string, string>;
24
+ }): Promise<Response>;
20
25
  mailRequest<T>(path: string, init?: {
21
26
  method?: string;
22
27
  body?: unknown;
23
28
  headers?: Record<string, string>;
24
29
  }): Promise<T>;
25
30
  private requestFrom;
31
+ private fetchFrom;
26
32
  }
27
33
  export declare class StackShiftAPIError extends Error {
28
34
  readonly status: number;
package/dist/client.js CHANGED
@@ -30,10 +30,22 @@ export class StackShift {
30
30
  async request(path, init = {}) {
31
31
  return this.requestFrom(this.baseUrl, path, init);
32
32
  }
33
+ requestRaw(path, init = {}) {
34
+ return this.fetchFrom(this.baseUrl, path, init);
35
+ }
33
36
  async mailRequest(path, init = {}) {
34
37
  return this.requestFrom(this.mailBaseUrl, path, init);
35
38
  }
36
39
  async requestFrom(baseUrl, path, init) {
40
+ const response = await this.fetchFrom(baseUrl, path, init);
41
+ const raw = await response.text();
42
+ const parsed = raw ? JSON.parse(raw) : null;
43
+ if (parsed && typeof parsed === 'object' && 'success' in parsed && 'data' in parsed) {
44
+ return parsed.data;
45
+ }
46
+ return parsed;
47
+ }
48
+ async fetchFrom(baseUrl, path, init) {
37
49
  const isFormData = typeof FormData !== 'undefined' && init.body instanceof FormData;
38
50
  const response = await this.fetcher(`${baseUrl}${path}`, {
39
51
  method: init.method ?? 'GET',
@@ -44,16 +56,13 @@ export class StackShift {
44
56
  }),
45
57
  body: (init.body === undefined ? undefined : isFormData ? init.body : JSON.stringify(stripUndefined(init.body))),
46
58
  });
47
- const raw = await response.text();
48
- const parsed = raw ? JSON.parse(raw) : null;
49
59
  if (!response.ok) {
60
+ const raw = await response.text();
61
+ const parsed = raw ? JSON.parse(raw) : null;
50
62
  const apiError = parsed?.error && typeof parsed.error === 'object' ? parsed.error : undefined;
51
63
  throw new StackShiftAPIError(apiError?.message ?? parsed?.message ?? parsed?.error ?? `StackShift API request failed with ${response.status}`, response.status, apiError?.code, apiError?.details);
52
64
  }
53
- if (parsed && typeof parsed === 'object' && 'success' in parsed && 'data' in parsed) {
54
- return parsed.data;
55
- }
56
- return parsed;
65
+ return response;
57
66
  }
58
67
  }
59
68
  export class StackShiftAPIError extends Error {