@stackshift-cloud/sdk 0.1.0 → 0.1.2

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 CHANGED
@@ -8,6 +8,129 @@ Official JavaScript and TypeScript SDK for StackShift.
8
8
  npm install @stackshift-cloud/sdk
9
9
  ```
10
10
 
11
+ Set your API key:
12
+
13
+ ```bash
14
+ STACKSHIFT_API_KEY=sk_live_xxx
15
+ ```
16
+
17
+ ## Send email
18
+
19
+ ```ts
20
+ import { StackShift } from '@stackshift-cloud/sdk'
21
+
22
+ const stackshift = new StackShift()
23
+
24
+ await stackshift.mail.send({
25
+ from: 'StackShift <noreply@mail.stackshift.cloud>',
26
+ to: 'ada@example.com',
27
+ subject: 'Welcome',
28
+ text: 'Welcome to StackShift.',
29
+ })
30
+ ```
31
+
32
+ HTML and idempotency keys are supported:
33
+
34
+ ```ts
35
+ const message = await stackshift.mail.send({
36
+ from: { email: 'noreply@mail.stackshift.cloud', name: 'StackShift' },
37
+ to: ['ada@example.com'],
38
+ subject: 'Verify your email',
39
+ html: '<p>Your code is 123456</p>',
40
+ text: 'Your code is 123456',
41
+ idempotencyKey: 'verify-email:user_123',
42
+ })
43
+
44
+ console.log(message.id, message.status, message.idempotencyStatus)
45
+ ```
46
+
47
+ Inspect sent messages:
48
+
49
+ ```ts
50
+ const messages = await stackshift.mail.messages.list({ status: 'sent', limit: 20 })
51
+ const detail = await stackshift.mail.messages.get(messages.data[0].id)
52
+ const attempts = await stackshift.mail.messages.attempts(detail.id)
53
+ const logs = await stackshift.mail.messages.logs(detail.id)
54
+ ```
55
+
56
+ In this MVP, `sent` means the message was accepted by StackShift's outbound MTA. It does not mean inbox delivery, opens, clicks, or spam placement. A `sent` message can later become `bounced` if StackShift receives a delivery-status notification.
57
+
58
+ ## Send a template
59
+
60
+ Templates are created and rendered by the StackShift backend. Variables use `{{name}}` syntax, and missing variables fail before a message is queued.
61
+
62
+ ```ts
63
+ await stackshift.mail.templates.create({
64
+ name: 'Welcome Email',
65
+ slug: 'welcome-email',
66
+ subject: 'Welcome, {{name}}',
67
+ html: '<h1>Welcome, {{name}}</h1>',
68
+ text: 'Welcome, {{name}}',
69
+ })
70
+
71
+ const preview = await stackshift.mail.templates.preview('welcome-email', {
72
+ data: { name: 'Ada' },
73
+ })
74
+
75
+ const message = await stackshift.mail.sendTemplate({
76
+ template: 'welcome-email',
77
+ to: 'ada@example.com',
78
+ from: 'Acme <noreply@acme.com>',
79
+ data: { name: 'Ada' },
80
+ idempotencyKey: 'welcome:user_123',
81
+ })
82
+
83
+ console.log(preview.subject, message.id)
84
+ ```
85
+
86
+ Updating template content creates a new version. You can inspect or reactivate versions with `templates.versions()`, `templates.getVersion()`, and `templates.activateVersion()`.
87
+
88
+ ## Bounces and suppressions
89
+
90
+ Hard bounces are automatically suppressed to protect sender reputation. Suppressions are workspace scoped and enforced by the StackShift backend before a message is queued.
91
+
92
+ ```ts
93
+ const suppressions = await stackshift.mail.suppressions.list()
94
+
95
+ await stackshift.mail.suppressions.create({
96
+ email: 'bad@example.com',
97
+ reason: 'manual',
98
+ })
99
+
100
+ await stackshift.mail.suppressions.delete('sup_123')
101
+
102
+ const bounces = await stackshift.mail.bounces.list({
103
+ type: 'hard',
104
+ limit: 20,
105
+ })
106
+
107
+ const messageBounces = await stackshift.mail.messages.bounces('msg_123')
108
+ ```
109
+
110
+ `bounced` means StackShift detected a delivery failure after handoff. Hard-bounced recipients are automatically suppressed; removing a suppression allows future sends to be queued again.
111
+
112
+ ## Verify a sending domain
113
+
114
+ Custom domains are workspace/API-key scoped. You do not pass a project ID or deploy an app first.
115
+
116
+ ```ts
117
+ const domain = await stackshift.mail.domains.create('acme.com')
118
+
119
+ console.log(domain.records)
120
+
121
+ await stackshift.mail.domains.verify(domain.id)
122
+
123
+ await stackshift.mail.send({
124
+ from: 'Acme <noreply@acme.com>',
125
+ to: 'user@example.com',
126
+ subject: 'Welcome',
127
+ html: '<h1>Welcome</h1>',
128
+ text: 'Welcome',
129
+ })
130
+ ```
131
+
132
+ Add the returned SPF, DKIM, and return-path DNS records before verifying. DMARC is recommended by default and may be required if your StackShift environment sets `MAIL_DMARC_REQUIRED=true`. DNS propagation can take time, so a missing or pending status usually means you should wait and verify again.
133
+
11
134
  ## Upload an asset
12
135
 
13
136
  ```ts
@@ -46,6 +169,57 @@ const signed = await stackshift.assets.signedUrl(asset.id, {
46
169
  console.log(signed.url)
47
170
  ```
48
171
 
172
+ ## Image transformations
173
+
174
+ Use built-in presets or create your own named transformations.
175
+
176
+ ```ts
177
+ import { StackShift, assetTransformOptions, getAssetTransformPreset } from '@stackshift-cloud/sdk'
178
+
179
+ const stackshift = new StackShift()
180
+ const hero = getAssetTransformPreset('hero')!
181
+ const heroOptions = assetTransformOptions(hero)
182
+
183
+ await stackshift.assets.createTransformation({ name: hero.name, ...heroOptions })
184
+
185
+ const named = stackshift.assets.namedUrl('asset_123', hero.name)
186
+ const signed = await stackshift.assets.signedTransformUrl('asset_123', {
187
+ ...heroOptions,
188
+ expiresIn: '10m',
189
+ })
190
+
191
+ await stackshift.assets.deleteTransformation('old-preset')
192
+ console.log(named, signed.url)
193
+ ```
194
+
195
+ ## NestJS
196
+
197
+ ```ts
198
+ import { Injectable } from '@nestjs/common'
199
+ import { StackShift } from '@stackshift-cloud/sdk'
200
+
201
+ @Injectable()
202
+ export class AssetsService {
203
+ private readonly stackshift = new StackShift({
204
+ apiKey: process.env.STACKSHIFT_API_KEY!,
205
+ })
206
+
207
+ upload(file: Express.Multer.File) {
208
+ return this.stackshift.assets.upload(
209
+ new Blob([file.buffer], { type: file.mimetype }),
210
+ {
211
+ bucket: 'uploads',
212
+ key: file.originalname,
213
+ visibility: 'private',
214
+ metadata: { originalName: file.originalname },
215
+ },
216
+ )
217
+ }
218
+ }
219
+ ```
220
+
221
+ For larger uploads, create a signed upload URL on your NestJS server and let the browser upload directly.
222
+
49
223
  ## Direct browser uploads
50
224
 
51
225
  Create a signed upload URL on your server:
@@ -70,4 +244,6 @@ await fetch(upload.url, {
70
244
  })
71
245
  ```
72
246
 
247
+ This is the recommended pattern for React, Next.js, TanStack Start, and any browser app. The browser never receives your StackShift API key.
248
+
73
249
  StackShift chooses storage nodes, writes to StackShift-owned disks, replicates internally, and returns CDN-ready asset URLs. SDK users never choose storage nodes and never receive disk paths.
package/dist/assets.d.ts CHANGED
@@ -1,5 +1,9 @@
1
- import type { StackShift } from './client';
2
- import type { Asset, AssetListOptions, AssetListResult, AssetSignedUploadUrl, AssetSignedUploadUrlOptions, AssetSignedUrl, AssetSignedUrlOptions, AssetUploadOptions } from './types';
1
+ import type { StackShift } from './client.js';
2
+ import type { Asset, AssetAIInfo, AssetCollection, AssetContentPolicy, AssetNamedTransformInput, AssetListOptions, AssetListResult, AssetSavedSearch, AssetTranscript, AssetUploadProgressCallbacks, AssetVersion, AssetSignedUploadUrl, AssetSignedUploadUrlOptions, AssetSignedTransformOptions, AssetSignedUrl, AssetSignedUrlOptions, AssetTransformOptions, AssetTransformPreset, AssetUploadOptions } from './types.js';
3
+ export declare const assetTransformPresets: AssetTransformPreset[];
4
+ export declare function getAssetTransformPreset(name: string): AssetTransformPreset | undefined;
5
+ export declare function assetTransformOptions(preset: AssetTransformPreset): AssetTransformOptions;
6
+ export declare function assetTransformSpec(options: AssetTransformOptions): string;
3
7
  export declare class AssetsClient {
4
8
  private readonly client;
5
9
  constructor(client: StackShift);
@@ -13,4 +17,84 @@ export declare class AssetsClient {
13
17
  signedUrl(assetId: string, options: AssetSignedUrlOptions): Promise<AssetSignedUrl>;
14
18
  signedUploadUrl(options?: AssetSignedUploadUrlOptions): Promise<AssetSignedUploadUrl>;
15
19
  createUploadSession(options?: AssetSignedUploadUrlOptions): Promise<AssetSignedUploadUrl>;
20
+ createChunkedUploadSession(options: AssetSignedUploadUrlOptions): Promise<AssetSignedUploadUrl>;
21
+ uploadChunk(uploadUrlOrToken: string, partNumber: number, chunk: Blob, callbacks?: AssetUploadProgressCallbacks): Promise<void>;
22
+ resumeUploadSession(sessionId: string): Promise<AssetSignedUploadUrl>;
23
+ completeUploadSession(sessionId: string): Promise<Asset>;
24
+ cancelUploadSession(sessionId: string): Promise<{
25
+ cancelled: boolean;
26
+ }>;
27
+ uploadWithProgress(file: Blob, options?: AssetSignedUploadUrlOptions, callbacks?: AssetUploadProgressCallbacks): Promise<Asset | AssetSignedUploadUrl>;
28
+ url(assetId: string, options: AssetTransformOptions): string;
29
+ namedUrl(assetId: string, name: string): string;
30
+ videoUrl(assetId: string, kind: 'hls' | 'poster' | 'mp4', profile?: string, token?: string): string;
31
+ versionUrl(assetId: string, versionId: string, token?: string): string;
32
+ branchUrl(assetId: string, branch: string, token?: string): string;
33
+ ai(assetId: string): Promise<AssetAIInfo>;
34
+ analyze(assetId: string): Promise<{
35
+ analysis_queued: boolean;
36
+ }>;
37
+ moderate(assetId: string): Promise<{
38
+ moderation_queued: boolean;
39
+ }>;
40
+ transcript(assetId: string): Promise<AssetTranscript>;
41
+ requestTranscript(assetId: string): Promise<{
42
+ transcript_queued: boolean;
43
+ }>;
44
+ requestSmartCrop(assetId: string): Promise<{
45
+ smart_crop_queued: boolean;
46
+ }>;
47
+ requestBackgroundRemoval(assetId: string): Promise<{
48
+ background_remove_queued: boolean;
49
+ }>;
50
+ versions(assetId: string): Promise<{
51
+ versions: AssetVersion[];
52
+ }>;
53
+ createBranch(assetId: string, name: string, fromVersionId?: string): Promise<unknown>;
54
+ restoreVersion(assetId: string, versionId: string): Promise<{
55
+ restored: boolean;
56
+ }>;
57
+ promoteBranch(assetId: string, branch: string): Promise<{
58
+ promoted: boolean;
59
+ }>;
60
+ collections(): Promise<{
61
+ collections: AssetCollection[];
62
+ }>;
63
+ createCollection(input: {
64
+ name: string;
65
+ description?: string;
66
+ asset_ids?: string[];
67
+ }): Promise<AssetCollection>;
68
+ updateCollection(collectionId: string, input: {
69
+ name: string;
70
+ description?: string;
71
+ asset_ids?: string[];
72
+ }): Promise<AssetCollection>;
73
+ deleteCollection(collectionId: string): Promise<{
74
+ deleted: boolean;
75
+ }>;
76
+ savedSearches(): Promise<{
77
+ saved_searches: AssetSavedSearch[];
78
+ }>;
79
+ saveSearch(input: {
80
+ name: string;
81
+ filters: Record<string, unknown>;
82
+ }): Promise<AssetSavedSearch>;
83
+ updateSavedSearch(searchId: string, input: {
84
+ name: string;
85
+ filters: Record<string, unknown>;
86
+ }): Promise<AssetSavedSearch>;
87
+ deleteSavedSearch(searchId: string): Promise<{
88
+ deleted: boolean;
89
+ }>;
90
+ getPolicy(): Promise<AssetContentPolicy>;
91
+ updatePolicy(policy: AssetContentPolicy): Promise<AssetContentPolicy>;
92
+ createTransformation(input: AssetNamedTransformInput): Promise<unknown>;
93
+ listTransformations(): Promise<unknown>;
94
+ deleteTransformation(name: string): Promise<{
95
+ deleted: boolean;
96
+ }>;
97
+ signedTransformUrl(assetId: string, options: AssetSignedTransformOptions): Promise<AssetSignedUrl>;
98
+ private originBaseUrl;
99
+ private uploadSingleWithProgress;
16
100
  }
package/dist/assets.js CHANGED
@@ -1,3 +1,44 @@
1
+ export const assetTransformPresets = [
2
+ preset('web-thumbnail', 'responsive-web', 'Compact grid thumbnail for libraries and dashboards.', 320, 180, 'fill', 'webp', 78),
3
+ preset('web-card', 'responsive-web', 'Balanced card image for product and content listings.', 640, 360, 'fill', 'webp', 82),
4
+ preset('feature-image', 'responsive-web', 'Wide content feature image with enough detail for retina screens.', 1200, 675, 'fill', 'webp', 84),
5
+ preset('hero', 'responsive-web', 'Large hero image for landing pages and page headers.', 1600, 900, 'fill', 'webp', 84),
6
+ preset('full-width-banner', 'responsive-web', 'Extra-wide banner for app shells and marketing sections.', 1920, 720, 'fill', 'webp', 82),
7
+ preset('open-graph', 'social', 'Share preview image for Open Graph cards.', 1200, 630, 'fill', 'jpeg', 85),
8
+ preset('square-post', 'social', 'Square image for social feeds.', 1080, 1080, 'fill', 'webp', 84),
9
+ preset('story', 'social', 'Vertical story format for mobile social placements.', 1080, 1920, 'fill', 'webp', 82),
10
+ preset('x-twitter-card', 'social', 'Landscape card optimized for X and Twitter previews.', 1200, 675, 'fill', 'jpeg', 84),
11
+ preset('linkedin-share', 'social', 'LinkedIn share preview dimensions.', 1200, 627, 'fill', 'jpeg', 84),
12
+ preset('product-square', 'product', 'Square product image with clean containment.', 1024, 1024, 'fit', 'webp', 86),
13
+ preset('product-card', 'product', 'Product listing image for commerce grids.', 640, 640, 'fit', 'webp', 84),
14
+ preset('product-zoom', 'product', 'High-detail product image for zoom interactions.', 2000, 2000, 'fit', 'jpeg', 90),
15
+ preset('gallery-thumb', 'product', 'Compact gallery thumbnail for product detail pages.', 400, 400, 'fill', 'webp', 82),
16
+ preset('avatar-small', 'avatar-profile', 'Small circular avatar source for dense UI.', 128, 128, 'fill', 'webp', 82),
17
+ preset('avatar-retina', 'avatar-profile', 'Retina avatar source for account and profile UI.', 256, 256, 'fill', 'webp', 84),
18
+ preset('profile-cover', 'avatar-profile', 'Wide profile cover image.', 1500, 500, 'fill', 'webp', 82),
19
+ preset('webp-auto', 'performance', 'Auto-sized WebP delivery with browser-friendly compression.', 1280, undefined, 'fit', 'webp', 78),
20
+ preset('avif-compressed', 'performance', 'Highly compressed AVIF for modern browsers.', 1280, undefined, 'fit', 'avif', 62),
21
+ preset('lqip', 'performance', 'Tiny low-quality placeholder for progressive image loading.', 48, undefined, 'fit', 'jpeg', 35),
22
+ preset('blur-placeholder', 'performance', 'Small placeholder candidate for blurred loading states.', 32, 32, 'fill', 'jpeg', 30),
23
+ preset('email-safe-600', 'email-docs', 'Email-safe content image with conservative width.', 600, undefined, 'fit', 'jpeg', 82),
24
+ preset('markdown-image', 'email-docs', 'Documentation image that fits most reading columns.', 800, undefined, 'fit', 'webp', 82),
25
+ preset('docs-screenshot', 'email-docs', 'Crisp screenshot size for guides and changelogs.', 1440, 900, 'fit', 'webp', 84),
26
+ ];
27
+ export function getAssetTransformPreset(name) {
28
+ return assetTransformPresets.find((preset) => preset.name === name);
29
+ }
30
+ export function assetTransformOptions(preset) {
31
+ return {
32
+ width: preset.width,
33
+ height: preset.height,
34
+ crop: preset.crop,
35
+ format: preset.format,
36
+ quality: preset.quality,
37
+ };
38
+ }
39
+ export function assetTransformSpec(options) {
40
+ return transformSpec(options);
41
+ }
1
42
  export class AssetsClient {
2
43
  client;
3
44
  constructor(client) {
@@ -14,6 +55,12 @@ export class AssetsClient {
14
55
  append(query, 'bucket', options.bucket);
15
56
  append(query, 'prefix', options.prefix);
16
57
  append(query, 'visibility', options.visibility);
58
+ append(query, 'query', options.query);
59
+ append(query, 'tag', options.tag);
60
+ append(query, 'mimeType', options.mimeType);
61
+ append(query, 'type', options.type);
62
+ append(query, 'folder', options.folder);
63
+ append(query, 'checksum', options.checksum);
17
64
  append(query, 'limit', options.limit);
18
65
  append(query, 'cursor', options.cursor);
19
66
  const suffix = query.toString();
@@ -55,11 +102,190 @@ export class AssetsClient {
55
102
  visibility: options.visibility,
56
103
  cacheControl: options.cacheControl,
57
104
  metadata: options.metadata,
105
+ mode: options.mode,
106
+ fileName: options.fileName,
107
+ fileSize: options.fileSize,
108
+ mimeType: options.mimeType,
109
+ checksumSHA256: options.checksumSHA256,
110
+ partSize: options.partSize,
58
111
  expiresIn: options.expiresIn,
59
112
  maxBytes: options.maxBytes,
60
113
  },
61
114
  });
62
115
  }
116
+ createChunkedUploadSession(options) {
117
+ return this.createUploadSession({ ...options, mode: 'chunked' });
118
+ }
119
+ uploadChunk(uploadUrlOrToken, partNumber, chunk, callbacks = {}) {
120
+ const target = uploadUrlOrToken.startsWith('http')
121
+ ? `${uploadUrlOrToken.replace(/\/$/, '')}/parts/${partNumber}`
122
+ : `${this.originBaseUrl()}/uploads/${encodeURIComponent(uploadUrlOrToken)}/parts/${partNumber}`;
123
+ return uploadBlob(target, chunk, callbacks, partNumber);
124
+ }
125
+ resumeUploadSession(sessionId) {
126
+ return this.client.request(`/assets/upload-sessions/${encodeURIComponent(sessionId)}`);
127
+ }
128
+ completeUploadSession(sessionId) {
129
+ return this.client.request(`/assets/upload-sessions/${encodeURIComponent(sessionId)}/complete`, {
130
+ method: 'POST',
131
+ });
132
+ }
133
+ cancelUploadSession(sessionId) {
134
+ return this.client.request(`/assets/upload-sessions/${encodeURIComponent(sessionId)}`, {
135
+ method: 'DELETE',
136
+ });
137
+ }
138
+ async uploadWithProgress(file, options = {}, callbacks = {}) {
139
+ const size = options.fileSize ?? file.size;
140
+ if (size <= defaultPartSize) {
141
+ return this.uploadSingleWithProgress(file, options, callbacks);
142
+ }
143
+ const session = await this.createChunkedUploadSession({
144
+ ...options,
145
+ fileSize: size,
146
+ fileName: options.fileName ?? blobName(file),
147
+ mimeType: options.mimeType ?? file.type,
148
+ });
149
+ const partSize = session.part_size ?? defaultPartSize;
150
+ for (let offset = 0, part = 1; offset < size; offset += partSize, part += 1) {
151
+ await this.uploadChunk(session.upload_session_id, part, file.slice(offset, offset + partSize), callbacks);
152
+ }
153
+ if (!session.session_id) {
154
+ throw new Error('Chunked upload session did not include a resumable session id');
155
+ }
156
+ return this.completeUploadSession(session.session_id);
157
+ }
158
+ url(assetId, options) {
159
+ return `${this.client.cdnBaseUrl}/assets/${encodeURIComponent(assetId)}/tr/${encodeURIComponent(transformSpec(options))}`;
160
+ }
161
+ namedUrl(assetId, name) {
162
+ return `${this.client.cdnBaseUrl}/t/${encodeURIComponent(name)}/assets/${encodeURIComponent(assetId)}`;
163
+ }
164
+ videoUrl(assetId, kind, profile = 'default', token) {
165
+ const visibilityPath = token ? '/private/assets' : '/assets';
166
+ const url = [
167
+ this.client.cdnBaseUrl,
168
+ visibilityPath,
169
+ '/',
170
+ encodeURIComponent(assetId),
171
+ '/video/',
172
+ encodeURIComponent(kind),
173
+ '/',
174
+ encodeURIComponent(profile),
175
+ ].join('');
176
+ return token ? `${url}?token=${encodeURIComponent(token)}` : url;
177
+ }
178
+ versionUrl(assetId, versionId, token) {
179
+ const prefix = token ? '/private/assets' : '/assets';
180
+ const value = `${this.client.cdnBaseUrl}${prefix}/${encodeURIComponent(assetId)}/versions/${encodeURIComponent(versionId)}`;
181
+ return token ? `${value}?token=${encodeURIComponent(token)}` : value;
182
+ }
183
+ branchUrl(assetId, branch, token) {
184
+ const prefix = token ? '/private/assets' : '/assets';
185
+ const value = `${this.client.cdnBaseUrl}${prefix}/${encodeURIComponent(assetId)}/branches/${encodeURIComponent(branch)}`;
186
+ return token ? `${value}?token=${encodeURIComponent(token)}` : value;
187
+ }
188
+ ai(assetId) {
189
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/ai`);
190
+ }
191
+ analyze(assetId) {
192
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/ai/analyze`, { method: 'POST' });
193
+ }
194
+ moderate(assetId) {
195
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/moderation`, { method: 'POST' });
196
+ }
197
+ transcript(assetId) {
198
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/transcript`);
199
+ }
200
+ requestTranscript(assetId) {
201
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/transcript`, { method: 'POST' });
202
+ }
203
+ requestSmartCrop(assetId) {
204
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/smart-crop`, { method: 'POST' });
205
+ }
206
+ requestBackgroundRemoval(assetId) {
207
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/background-remove`, { method: 'POST' });
208
+ }
209
+ versions(assetId) {
210
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/versions`);
211
+ }
212
+ createBranch(assetId, name, fromVersionId) {
213
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/branches`, {
214
+ method: 'POST',
215
+ body: { name, from_version_id: fromVersionId },
216
+ });
217
+ }
218
+ restoreVersion(assetId, versionId) {
219
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/versions/${encodeURIComponent(versionId)}/restore`, { method: 'POST' });
220
+ }
221
+ promoteBranch(assetId, branch) {
222
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/branches/${encodeURIComponent(branch)}/promote`, { method: 'POST' });
223
+ }
224
+ collections() {
225
+ return this.client.request('/assets/collections');
226
+ }
227
+ createCollection(input) {
228
+ return this.client.request('/assets/collections', { method: 'POST', body: input });
229
+ }
230
+ updateCollection(collectionId, input) {
231
+ return this.client.request(`/assets/collections/${encodeURIComponent(collectionId)}`, { method: 'PUT', body: input });
232
+ }
233
+ deleteCollection(collectionId) {
234
+ return this.client.request(`/assets/collections/${encodeURIComponent(collectionId)}`, { method: 'DELETE' });
235
+ }
236
+ savedSearches() {
237
+ return this.client.request('/assets/saved-searches');
238
+ }
239
+ saveSearch(input) {
240
+ return this.client.request('/assets/saved-searches', { method: 'POST', body: input });
241
+ }
242
+ updateSavedSearch(searchId, input) {
243
+ return this.client.request(`/assets/saved-searches/${encodeURIComponent(searchId)}`, { method: 'PUT', body: input });
244
+ }
245
+ deleteSavedSearch(searchId) {
246
+ return this.client.request(`/assets/saved-searches/${encodeURIComponent(searchId)}`, { method: 'DELETE' });
247
+ }
248
+ getPolicy() {
249
+ return this.client.request('/assets/policy');
250
+ }
251
+ updatePolicy(policy) {
252
+ return this.client.request('/assets/policy', {
253
+ method: 'PUT',
254
+ body: policy,
255
+ });
256
+ }
257
+ createTransformation(input) {
258
+ return this.client.request('/assets/transformations', {
259
+ method: 'POST',
260
+ body: input,
261
+ });
262
+ }
263
+ listTransformations() {
264
+ return this.client.request('/assets/transformations');
265
+ }
266
+ deleteTransformation(name) {
267
+ return this.client.request(`/assets/transformations/${encodeURIComponent(name)}`, {
268
+ method: 'DELETE',
269
+ });
270
+ }
271
+ signedTransformUrl(assetId, options) {
272
+ return this.client.request(`/assets/${encodeURIComponent(assetId)}/transform-url`, {
273
+ method: 'POST',
274
+ body: transformBody(options),
275
+ });
276
+ }
277
+ originBaseUrl() {
278
+ return this.client.baseUrl.replace(/\/api\/v1$/, '');
279
+ }
280
+ async uploadSingleWithProgress(file, options, callbacks) {
281
+ const session = await this.createUploadSession({
282
+ ...options,
283
+ mode: 'single',
284
+ maxBytes: options.maxBytes ?? file.size,
285
+ });
286
+ await uploadBlob(session.url, file, callbacks);
287
+ return session;
288
+ }
63
289
  }
64
290
  function assetForm(file, options) {
65
291
  const body = new FormData();
@@ -79,3 +305,58 @@ function append(target, key, value) {
79
305
  target.append(key, String(value));
80
306
  }
81
307
  }
308
+ function transformSpec(options) {
309
+ const parts = [];
310
+ if (options.width)
311
+ parts.push(`w_${options.width}`);
312
+ if (options.height)
313
+ parts.push(`h_${options.height}`);
314
+ parts.push(`c_${options.crop ?? 'fit'}`);
315
+ parts.push(`f_${options.format ?? 'auto'}`);
316
+ parts.push(`q_${options.quality ?? 'auto'}`);
317
+ return parts.join(',');
318
+ }
319
+ function preset(name, category, description, width, height, crop = 'fit', format = 'auto', quality = 'auto') {
320
+ return { id: name, name, category, description, width, height, crop, format, quality };
321
+ }
322
+ function transformBody(options) {
323
+ return {
324
+ width: options.width,
325
+ height: options.height,
326
+ crop: options.crop,
327
+ format: options.format,
328
+ quality: options.quality,
329
+ expiresIn: options.expiresIn,
330
+ };
331
+ }
332
+ const defaultPartSize = 5 * 1024 * 1024;
333
+ function blobName(file) {
334
+ return file.name;
335
+ }
336
+ function uploadBlob(url, blob, callbacks, partNumber) {
337
+ if (typeof XMLHttpRequest === 'undefined') {
338
+ return fetch(url, { method: 'PUT', body: blob }).then((response) => {
339
+ if (!response.ok)
340
+ throw new Error(`Upload failed with ${response.status}`);
341
+ });
342
+ }
343
+ return new Promise((resolve, reject) => {
344
+ const xhr = new XMLHttpRequest();
345
+ xhr.open('PUT', url);
346
+ xhr.upload.onprogress = (event) => {
347
+ const total = event.total || blob.size;
348
+ const progress = { loaded: event.loaded, total, percent: total ? Math.round((event.loaded / total) * 100) : 0 };
349
+ callbacks.onProgress?.(progress);
350
+ if (partNumber)
351
+ callbacks.onPartProgress?.(partNumber, progress);
352
+ };
353
+ xhr.onload = () => {
354
+ if (xhr.status >= 200 && xhr.status < 300)
355
+ resolve();
356
+ else
357
+ reject(new Error(`Upload failed with ${xhr.status}`));
358
+ };
359
+ xhr.onerror = () => reject(new Error('Upload failed'));
360
+ xhr.send(blob);
361
+ });
362
+ }
package/dist/client.d.ts CHANGED
@@ -1,11 +1,15 @@
1
- import { AssetsClient } from './assets';
2
- import { ProjectsClient } from './projects';
3
- import type { StackShiftOptions } from './types';
1
+ import { AssetsClient } from './assets.js';
2
+ import { MailClient } from './mail.js';
3
+ import { ProjectsClient } from './projects.js';
4
+ import type { StackShiftOptions } from './types.js';
4
5
  export declare class StackShift {
5
6
  readonly apiKey: string;
6
7
  readonly baseUrl: string;
8
+ readonly mailBaseUrl: string;
9
+ readonly cdnBaseUrl: string;
7
10
  readonly projects: ProjectsClient;
8
11
  readonly assets: AssetsClient;
12
+ readonly mail: MailClient;
9
13
  private readonly fetcher;
10
14
  constructor(options?: StackShiftOptions);
11
15
  request<T>(path: string, init?: {
@@ -13,4 +17,16 @@ export declare class StackShift {
13
17
  body?: unknown;
14
18
  headers?: Record<string, string>;
15
19
  }): Promise<T>;
20
+ mailRequest<T>(path: string, init?: {
21
+ method?: string;
22
+ body?: unknown;
23
+ headers?: Record<string, string>;
24
+ }): Promise<T>;
25
+ private requestFrom;
26
+ }
27
+ export declare class StackShiftAPIError extends Error {
28
+ readonly status: number;
29
+ readonly code?: string | undefined;
30
+ readonly details?: unknown | undefined;
31
+ constructor(message: string, status: number, code?: string | undefined, details?: unknown | undefined);
16
32
  }