@stackshift-cloud/sdk 0.1.3 → 1.0.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.
- package/README.md +3 -3
- package/dist/assets-framework.d.ts +21 -0
- package/dist/assets-framework.js +34 -0
- package/dist/assets.d.ts +86 -21
- package/dist/assets.js +261 -40
- package/dist/generated/assets-contract.d.ts +329 -0
- package/dist/generated/assets-contract.js +66 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +4 -1
- package/dist/mail-advanced-types.d.ts +25 -3
- package/dist/mail-advanced.d.ts +8 -1
- package/dist/mail-advanced.js +9 -0
- package/dist/mail-campaigns.d.ts +76 -0
- package/dist/mail-campaigns.js +42 -0
- package/dist/mail-diagnostics.d.ts +23 -0
- package/dist/mail-diagnostics.js +1 -0
- package/dist/mail-events-webhooks.d.ts +3 -2
- package/dist/mail-events-webhooks.js +10 -2
- package/dist/mail-exports.d.ts +32 -0
- package/dist/mail-exports.js +18 -0
- package/dist/mail-streams.d.ts +27 -0
- package/dist/mail-streams.js +12 -0
- package/dist/mail-webhook-types.d.ts +12 -1
- package/dist/mail.d.ts +14 -2
- package/dist/mail.js +22 -2
- package/dist/types.d.ts +217 -4
- package/package.json +2 -2
- /package/dist/{mail-phase4-types.d.ts → mail-delivery-types.d.ts} +0 -0
- /package/dist/{mail-phase4-types.js → mail-delivery-types.js} +0 -0
package/README.md
CHANGED
|
@@ -44,16 +44,16 @@ const message = await stackshift.mail.send({
|
|
|
44
44
|
console.log(message.id, message.status, message.idempotencyStatus)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Inspect
|
|
47
|
+
Inspect MTA-accepted messages:
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
|
-
const messages = await stackshift.mail.messages.list({ status: '
|
|
50
|
+
const messages = await stackshift.mail.messages.list({ status: 'mta_accepted', limit: 20 })
|
|
51
51
|
const detail = await stackshift.mail.messages.get(messages.data[0].id)
|
|
52
52
|
const attempts = await stackshift.mail.messages.attempts(detail.id)
|
|
53
53
|
const logs = await stackshift.mail.messages.logs(detail.id)
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
`mta_accepted` means StackShift's outbound MTA accepted the message. `delivered` means the recipient MX accepted a recipient; it does not claim inbox placement or human receipt. Recipient records remain authoritative for mixed outcomes.
|
|
57
57
|
|
|
58
58
|
## Send a template
|
|
59
59
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AssetsClient } from './assets.js';
|
|
2
|
+
import type { AssetSignedTransformOptions, AssetSignedUploadUrl, AssetTransformOptions } from './types.js';
|
|
3
|
+
export interface ResponsiveImageOptions extends Omit<AssetSignedTransformOptions, 'width'> {
|
|
4
|
+
widths: number[];
|
|
5
|
+
sizes?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ResponsiveImageDescriptor {
|
|
8
|
+
src: string;
|
|
9
|
+
srcSet: string;
|
|
10
|
+
sizes: string;
|
|
11
|
+
widthUrls: Record<number, string>;
|
|
12
|
+
}
|
|
13
|
+
/** Shared by Next.js server actions, React/TanStack loaders, and NestJS services. */
|
|
14
|
+
export declare function createResponsiveImage(assets: AssetsClient, assetId: string, options: ResponsiveImageOptions): Promise<ResponsiveImageDescriptor>;
|
|
15
|
+
/** Uploads through a short-lived session without exposing the account API key. */
|
|
16
|
+
export declare function uploadBrowserFile(session: AssetSignedUploadUrl, file: Blob, fetcher?: typeof fetch): Promise<Response>;
|
|
17
|
+
export declare function namedResponsiveSource(assets: AssetsClient, assetId: string, preset: string, widths: number[]): string;
|
|
18
|
+
export type ArtDirectionSource = {
|
|
19
|
+
media: string;
|
|
20
|
+
transform: AssetTransformOptions;
|
|
21
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Shared by Next.js server actions, React/TanStack loaders, and NestJS services. */
|
|
2
|
+
export async function createResponsiveImage(assets, assetId, options) {
|
|
3
|
+
const widths = [...new Set(options.widths)].filter((width) => Number.isSafeInteger(width) && width > 0).sort((a, b) => a - b);
|
|
4
|
+
if (widths.length === 0 || widths.length > 12)
|
|
5
|
+
throw new Error('Responsive images require 1-12 positive integer widths');
|
|
6
|
+
const { widths: _widths, sizes = '100vw', ...transform } = options;
|
|
7
|
+
const signed = await Promise.all(widths.map(async (width) => [width, await assets.signedTransformUrl(assetId, { ...transform, width })]));
|
|
8
|
+
const widthUrls = Object.fromEntries(signed.map(([width, result]) => [width, result.url]));
|
|
9
|
+
return {
|
|
10
|
+
src: widthUrls[widths[widths.length - 1]],
|
|
11
|
+
srcSet: widths.map((width) => `${widthUrls[width]} ${width}w`).join(', '),
|
|
12
|
+
sizes,
|
|
13
|
+
widthUrls,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Uploads through a short-lived session without exposing the account API key. */
|
|
17
|
+
export async function uploadBrowserFile(session, file, fetcher = fetch) {
|
|
18
|
+
if (session.method !== 'PUT')
|
|
19
|
+
throw new Error(`Unsupported upload method: ${session.method}`);
|
|
20
|
+
if (file.size > session.max_bytes)
|
|
21
|
+
throw new Error(`File exceeds signed upload limit of ${session.max_bytes} bytes`);
|
|
22
|
+
const response = await fetcher(session.url, {
|
|
23
|
+
method: session.method,
|
|
24
|
+
body: file,
|
|
25
|
+
headers: file.type ? { 'Content-Type': file.type } : undefined,
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok)
|
|
28
|
+
throw new Error(`StackShift browser upload failed with HTTP ${response.status}`);
|
|
29
|
+
return response;
|
|
30
|
+
}
|
|
31
|
+
export function namedResponsiveSource(assets, assetId, preset, widths) {
|
|
32
|
+
const clean = [...new Set(widths)].filter((width) => Number.isSafeInteger(width) && width > 0).sort((a, b) => a - b);
|
|
33
|
+
return clean.map((width) => `${assets.namedUrl(assetId, `${preset}-${width}`)} ${width}w`).join(', ');
|
|
34
|
+
}
|
package/dist/assets.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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';
|
|
2
|
+
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
3
|
export declare const assetTransformPresets: AssetTransformPreset[];
|
|
4
4
|
export declare function getAssetTransformPreset(name: string): AssetTransformPreset | undefined;
|
|
5
5
|
export declare function assetTransformOptions(preset: AssetTransformPreset): AssetTransformOptions;
|
|
@@ -10,10 +10,8 @@ export declare class AssetsClient {
|
|
|
10
10
|
upload(file: Blob, options?: AssetUploadOptions): Promise<Asset>;
|
|
11
11
|
list(options?: AssetListOptions): Promise<AssetListResult>;
|
|
12
12
|
get(assetId: string): Promise<Asset>;
|
|
13
|
-
delete(assetId: string): Promise<
|
|
14
|
-
|
|
15
|
-
}>;
|
|
16
|
-
replace(assetId: string, file: Blob, options?: Pick<AssetUploadOptions, 'cacheControl' | 'metadata'>): Promise<Asset>;
|
|
13
|
+
delete(assetId: string, revision: number, idempotencyKey?: string): Promise<AssetJobSubmission>;
|
|
14
|
+
replace(assetId: string, revision: number, file: Blob, options?: Pick<AssetUploadOptions, 'cacheControl' | 'metadata'>): Promise<Asset>;
|
|
17
15
|
signedUrl(assetId: string, options: AssetSignedUrlOptions): Promise<AssetSignedUrl>;
|
|
18
16
|
signedUploadUrl(options?: AssetSignedUploadUrlOptions): Promise<AssetSignedUploadUrl>;
|
|
19
17
|
createUploadSession(options?: AssetSignedUploadUrlOptions): Promise<AssetSignedUploadUrl>;
|
|
@@ -24,29 +22,36 @@ export declare class AssetsClient {
|
|
|
24
22
|
cancelUploadSession(sessionId: string): Promise<{
|
|
25
23
|
cancelled: boolean;
|
|
26
24
|
}>;
|
|
25
|
+
listBuckets(): Promise<{
|
|
26
|
+
buckets: AssetBucket[];
|
|
27
|
+
}>;
|
|
28
|
+
createBucket(input: AssetBucketInput & {
|
|
29
|
+
name: string;
|
|
30
|
+
}): Promise<AssetBucket>;
|
|
31
|
+
updateBucket(bucketId: string, revision: number, input: AssetBucketInput): Promise<AssetBucket>;
|
|
32
|
+
deleteBucket(bucketId: string, revision: number): Promise<void>;
|
|
33
|
+
setTags(assetId: string, revision: number, tags: string[]): Promise<{
|
|
34
|
+
tags: string[];
|
|
35
|
+
revision: number;
|
|
36
|
+
}>;
|
|
37
|
+
patchMetadata(assetId: string, revision: number, metadata: Record<string, unknown>): Promise<{
|
|
38
|
+
updated: boolean;
|
|
39
|
+
revision: number;
|
|
40
|
+
}>;
|
|
41
|
+
patch(assetId: string, revision: number, input: AssetPatchInput): Promise<Asset>;
|
|
27
42
|
uploadWithProgress(file: Blob, options?: AssetSignedUploadUrlOptions, callbacks?: AssetUploadProgressCallbacks): Promise<Asset | AssetSignedUploadUrl>;
|
|
28
43
|
url(assetId: string, options: AssetTransformOptions): string;
|
|
29
44
|
namedUrl(assetId: string, name: string): string;
|
|
30
|
-
videoUrl(assetId: string, kind: 'hls' | 'poster' | 'mp4', profile?: string, token?: string): string;
|
|
45
|
+
videoUrl(assetId: string, kind: 'hls' | 'poster' | 'thumbnail' | 'mp4', profile?: string, token?: string): string;
|
|
31
46
|
versionUrl(assetId: string, versionId: string, token?: string): string;
|
|
32
47
|
branchUrl(assetId: string, branch: string, token?: string): string;
|
|
33
48
|
ai(assetId: string): Promise<AssetAIInfo>;
|
|
34
|
-
analyze(assetId: string): Promise<
|
|
35
|
-
|
|
36
|
-
}>;
|
|
37
|
-
moderate(assetId: string): Promise<{
|
|
38
|
-
moderation_queued: boolean;
|
|
39
|
-
}>;
|
|
49
|
+
analyze(assetId: string, idempotencyKey?: string): Promise<AssetJobSubmission>;
|
|
50
|
+
moderate(assetId: string, idempotencyKey?: string): Promise<AssetJobSubmission>;
|
|
40
51
|
transcript(assetId: string): Promise<AssetTranscript>;
|
|
41
|
-
requestTranscript(assetId: string): Promise<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
requestSmartCrop(assetId: string): Promise<{
|
|
45
|
-
smart_crop_queued: boolean;
|
|
46
|
-
}>;
|
|
47
|
-
requestBackgroundRemoval(assetId: string): Promise<{
|
|
48
|
-
background_remove_queued: boolean;
|
|
49
|
-
}>;
|
|
52
|
+
requestTranscript(assetId: string, idempotencyKey?: string): Promise<AssetJobSubmission>;
|
|
53
|
+
requestSmartCrop(assetId: string, idempotencyKey?: string): Promise<AssetJobSubmission>;
|
|
54
|
+
requestBackgroundRemoval(assetId: string, idempotencyKey?: string): Promise<AssetJobSubmission>;
|
|
50
55
|
versions(assetId: string): Promise<{
|
|
51
56
|
versions: AssetVersion[];
|
|
52
57
|
}>;
|
|
@@ -95,6 +100,66 @@ export declare class AssetsClient {
|
|
|
95
100
|
deleted: boolean;
|
|
96
101
|
}>;
|
|
97
102
|
signedTransformUrl(assetId: string, options: AssetSignedTransformOptions): Promise<AssetSignedUrl>;
|
|
103
|
+
getAIConfig(): Promise<AssetAIAccountConfig>;
|
|
104
|
+
updateAIConfig(revision: number, enabledActions: string[], monthlySpendCapMicros: number): Promise<import("./types.js").AssetAIPolicy>;
|
|
105
|
+
job(jobId: string): Promise<AssetJob>;
|
|
106
|
+
cancelJob(jobId: string): Promise<AssetJob>;
|
|
107
|
+
createLifecycleRule(input: {
|
|
108
|
+
name: string;
|
|
109
|
+
prefix?: string;
|
|
110
|
+
action: 'delete' | 'expire_versions';
|
|
111
|
+
age_days: number;
|
|
112
|
+
enabled?: boolean;
|
|
113
|
+
}): Promise<AssetLifecycleRule>;
|
|
114
|
+
listLifecycleRules(): Promise<{
|
|
115
|
+
lifecycle_rules: AssetLifecycleRule[];
|
|
116
|
+
}>;
|
|
117
|
+
deleteLifecycleRule(ruleId: string): Promise<{
|
|
118
|
+
deleted: boolean;
|
|
119
|
+
}>;
|
|
120
|
+
createDomain(domain: string): Promise<AssetCustomDomain>;
|
|
121
|
+
listDomains(): Promise<{
|
|
122
|
+
domains: AssetCustomDomain[];
|
|
123
|
+
}>;
|
|
124
|
+
verifyDomain(domainId: string): Promise<AssetCustomDomain>;
|
|
125
|
+
deleteDomain(domainId: string): Promise<{
|
|
126
|
+
deleted: boolean;
|
|
127
|
+
}>;
|
|
128
|
+
analytics(): Promise<AssetUsageSummary>;
|
|
129
|
+
usageSummary(): Promise<AssetUsageSummary>;
|
|
130
|
+
events(limit?: number): Promise<{
|
|
131
|
+
events: AssetEvent[];
|
|
132
|
+
total: number;
|
|
133
|
+
}>;
|
|
134
|
+
createWebhook(url: string, eventTypes?: string[]): Promise<{
|
|
135
|
+
webhook: AssetWebhook;
|
|
136
|
+
secret: string;
|
|
137
|
+
}>;
|
|
138
|
+
listWebhooks(): Promise<{
|
|
139
|
+
webhooks: AssetWebhook[];
|
|
140
|
+
}>;
|
|
141
|
+
deleteWebhook(webhookId: string): Promise<{
|
|
142
|
+
deleted: boolean;
|
|
143
|
+
}>;
|
|
144
|
+
listWebhookDeliveries(webhookId: string, limit?: number): Promise<{
|
|
145
|
+
deliveries: AssetWebhookDelivery[];
|
|
146
|
+
}>;
|
|
147
|
+
retryWebhookDelivery(deliveryId: string): Promise<AssetWebhookDelivery>;
|
|
148
|
+
purge(assetId: string): Promise<{
|
|
149
|
+
purged: boolean;
|
|
150
|
+
asset_id: string;
|
|
151
|
+
}>;
|
|
152
|
+
bulk(input: {
|
|
153
|
+
action: 'delete' | 'visibility' | 'add_tags';
|
|
154
|
+
items: Array<{
|
|
155
|
+
asset_id: string;
|
|
156
|
+
revision: number;
|
|
157
|
+
}>;
|
|
158
|
+
visibility?: string;
|
|
159
|
+
tags?: string[];
|
|
160
|
+
idempotencyKey?: string;
|
|
161
|
+
}): Promise<AssetJobSubmission>;
|
|
98
162
|
private originBaseUrl;
|
|
163
|
+
private requestAssetJob;
|
|
99
164
|
private uploadSingleWithProgress;
|
|
100
165
|
}
|
package/dist/assets.js
CHANGED
|
@@ -57,10 +57,17 @@ export class AssetsClient {
|
|
|
57
57
|
append(query, 'visibility', options.visibility);
|
|
58
58
|
append(query, 'query', options.query);
|
|
59
59
|
append(query, 'tag', options.tag);
|
|
60
|
-
append(query, '
|
|
60
|
+
append(query, 'mime_type', options.mimeType);
|
|
61
61
|
append(query, 'type', options.type);
|
|
62
62
|
append(query, 'folder', options.folder);
|
|
63
63
|
append(query, 'checksum', options.checksum);
|
|
64
|
+
append(query, 'status', options.status);
|
|
65
|
+
append(query, 'scan_status', options.scanStatus);
|
|
66
|
+
append(query, 'replication_status', options.replicationStatus);
|
|
67
|
+
append(query, 'moderation_status', options.moderationStatus);
|
|
68
|
+
append(query, 'quarantine_status', options.quarantineStatus);
|
|
69
|
+
append(query, 'sort', options.sort);
|
|
70
|
+
append(query, 'direction', options.direction);
|
|
64
71
|
append(query, 'limit', options.limit);
|
|
65
72
|
append(query, 'cursor', options.cursor);
|
|
66
73
|
const suffix = query.toString();
|
|
@@ -69,23 +76,25 @@ export class AssetsClient {
|
|
|
69
76
|
get(assetId) {
|
|
70
77
|
return this.client.request(`/assets/${encodeURIComponent(assetId)}`);
|
|
71
78
|
}
|
|
72
|
-
delete(assetId) {
|
|
79
|
+
delete(assetId, revision, idempotencyKey) {
|
|
73
80
|
return this.client.request(`/assets/${encodeURIComponent(assetId)}`, {
|
|
74
81
|
method: 'DELETE',
|
|
82
|
+
headers: { 'If-Match': `"${revision}"`, ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}) },
|
|
75
83
|
});
|
|
76
84
|
}
|
|
77
|
-
replace(assetId, file, options = {}) {
|
|
85
|
+
replace(assetId, revision, file, options = {}) {
|
|
78
86
|
return this.client.request(`/assets/${encodeURIComponent(assetId)}/replace`, {
|
|
79
87
|
method: 'PUT',
|
|
80
88
|
body: assetForm(file, options),
|
|
89
|
+
headers: { 'If-Match': `"${revision}"` },
|
|
81
90
|
});
|
|
82
91
|
}
|
|
83
92
|
signedUrl(assetId, options) {
|
|
84
93
|
return this.client.request(`/assets/${encodeURIComponent(assetId)}/signed-url`, {
|
|
85
94
|
method: 'POST',
|
|
86
95
|
body: {
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
expires_in: options.expiresIn,
|
|
97
|
+
max_downloads: options.maxDownloads,
|
|
89
98
|
},
|
|
90
99
|
});
|
|
91
100
|
}
|
|
@@ -95,32 +104,33 @@ export class AssetsClient {
|
|
|
95
104
|
createUploadSession(options = {}) {
|
|
96
105
|
return this.client.request('/assets/upload-sessions', {
|
|
97
106
|
method: 'POST',
|
|
107
|
+
headers: options.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : undefined,
|
|
98
108
|
body: {
|
|
99
109
|
bucket: options.bucket,
|
|
100
110
|
key: options.key,
|
|
101
111
|
folder: options.folder,
|
|
102
112
|
visibility: options.visibility,
|
|
103
|
-
|
|
113
|
+
cache_control: options.cacheControl,
|
|
104
114
|
metadata: options.metadata,
|
|
105
115
|
mode: options.mode,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
file_name: options.fileName,
|
|
117
|
+
file_size: options.fileSize,
|
|
118
|
+
mime_type: options.mimeType,
|
|
119
|
+
checksum_sha256: options.checksumSHA256,
|
|
120
|
+
part_size: options.partSize,
|
|
121
|
+
expires_in: options.expiresIn,
|
|
122
|
+
max_bytes: options.maxBytes,
|
|
113
123
|
},
|
|
114
124
|
});
|
|
115
125
|
}
|
|
116
126
|
createChunkedUploadSession(options) {
|
|
117
127
|
return this.createUploadSession({ ...options, mode: 'chunked' });
|
|
118
128
|
}
|
|
119
|
-
uploadChunk(uploadUrlOrToken, partNumber, chunk, callbacks = {}) {
|
|
129
|
+
async uploadChunk(uploadUrlOrToken, partNumber, chunk, callbacks = {}) {
|
|
120
130
|
const target = uploadUrlOrToken.startsWith('http')
|
|
121
131
|
? `${uploadUrlOrToken.replace(/\/$/, '')}/parts/${partNumber}`
|
|
122
132
|
: `${this.originBaseUrl()}/uploads/${encodeURIComponent(uploadUrlOrToken)}/parts/${partNumber}`;
|
|
123
|
-
return uploadBlob(target, chunk, callbacks, partNumber);
|
|
133
|
+
return uploadBlob(target, chunk, callbacks, partNumber, { 'X-Content-SHA256': await sha256Blob(chunk) });
|
|
124
134
|
}
|
|
125
135
|
resumeUploadSession(sessionId) {
|
|
126
136
|
return this.client.request(`/assets/upload-sessions/${encodeURIComponent(sessionId)}`);
|
|
@@ -135,25 +145,101 @@ export class AssetsClient {
|
|
|
135
145
|
method: 'DELETE',
|
|
136
146
|
});
|
|
137
147
|
}
|
|
148
|
+
listBuckets() {
|
|
149
|
+
return this.client.request('/assets/buckets');
|
|
150
|
+
}
|
|
151
|
+
createBucket(input) {
|
|
152
|
+
return this.client.request('/assets/buckets', { method: 'POST', body: input });
|
|
153
|
+
}
|
|
154
|
+
updateBucket(bucketId, revision, input) {
|
|
155
|
+
return this.client.request(`/assets/buckets/${encodeURIComponent(bucketId)}`, {
|
|
156
|
+
method: 'PUT', body: input, headers: { 'If-Match': `"${revision}"` },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
deleteBucket(bucketId, revision) {
|
|
160
|
+
return this.client.request(`/assets/buckets/${encodeURIComponent(bucketId)}`, {
|
|
161
|
+
method: 'DELETE', headers: { 'If-Match': `"${revision}"` },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
setTags(assetId, revision, tags) {
|
|
165
|
+
return this.client.request(`/assets/${encodeURIComponent(assetId)}/tags`, {
|
|
166
|
+
method: 'PUT', body: { tags }, headers: { 'If-Match': `"${revision}"` },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
patchMetadata(assetId, revision, metadata) {
|
|
170
|
+
return this.client.request(`/assets/${encodeURIComponent(assetId)}/metadata`, {
|
|
171
|
+
method: 'PATCH', body: { metadata }, headers: { 'If-Match': `"${revision}"` },
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
patch(assetId, revision, input) {
|
|
175
|
+
return this.client.request(`/assets/${encodeURIComponent(assetId)}`, {
|
|
176
|
+
method: 'PATCH', body: input, headers: { 'If-Match': `"${revision}"` },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
138
179
|
async uploadWithProgress(file, options = {}, callbacks = {}) {
|
|
139
180
|
const size = options.fileSize ?? file.size;
|
|
140
181
|
if (size <= defaultPartSize) {
|
|
141
182
|
return this.uploadSingleWithProgress(file, options, callbacks);
|
|
142
183
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
throwIfAborted(callbacks.signal);
|
|
185
|
+
const session = options.uploadSessionId
|
|
186
|
+
? await this.resumeUploadSession(options.uploadSessionId)
|
|
187
|
+
: await this.createChunkedUploadSession({
|
|
188
|
+
...options,
|
|
189
|
+
fileSize: size,
|
|
190
|
+
fileName: options.fileName ?? blobName(file),
|
|
191
|
+
mimeType: options.mimeType ?? file.type,
|
|
192
|
+
});
|
|
149
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 = [];
|
|
150
201
|
for (let offset = 0, part = 1; offset < size; offset += partSize, part += 1) {
|
|
151
|
-
|
|
202
|
+
if (!completed.has(part))
|
|
203
|
+
tasks.push({ part, chunk: file.slice(offset, offset + partSize) });
|
|
152
204
|
}
|
|
153
|
-
|
|
154
|
-
|
|
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;
|
|
155
242
|
}
|
|
156
|
-
return this.completeUploadSession(session.session_id);
|
|
157
243
|
}
|
|
158
244
|
url(assetId, options) {
|
|
159
245
|
return `${this.client.cdnBaseUrl}/assets/${encodeURIComponent(assetId)}/tr/${encodeURIComponent(transformSpec(options))}`;
|
|
@@ -188,23 +274,23 @@ export class AssetsClient {
|
|
|
188
274
|
ai(assetId) {
|
|
189
275
|
return this.client.request(`/assets/${encodeURIComponent(assetId)}/ai`);
|
|
190
276
|
}
|
|
191
|
-
analyze(assetId) {
|
|
192
|
-
return this.
|
|
277
|
+
analyze(assetId, idempotencyKey) {
|
|
278
|
+
return this.requestAssetJob(`/assets/${encodeURIComponent(assetId)}/ai/analyze`, idempotencyKey);
|
|
193
279
|
}
|
|
194
|
-
moderate(assetId) {
|
|
195
|
-
return this.
|
|
280
|
+
moderate(assetId, idempotencyKey) {
|
|
281
|
+
return this.requestAssetJob(`/assets/${encodeURIComponent(assetId)}/moderation`, idempotencyKey);
|
|
196
282
|
}
|
|
197
283
|
transcript(assetId) {
|
|
198
284
|
return this.client.request(`/assets/${encodeURIComponent(assetId)}/transcript`);
|
|
199
285
|
}
|
|
200
|
-
requestTranscript(assetId) {
|
|
201
|
-
return this.
|
|
286
|
+
requestTranscript(assetId, idempotencyKey) {
|
|
287
|
+
return this.requestAssetJob(`/assets/${encodeURIComponent(assetId)}/transcript`, idempotencyKey);
|
|
202
288
|
}
|
|
203
|
-
requestSmartCrop(assetId) {
|
|
204
|
-
return this.
|
|
289
|
+
requestSmartCrop(assetId, idempotencyKey) {
|
|
290
|
+
return this.requestAssetJob(`/assets/${encodeURIComponent(assetId)}/smart-crop`, idempotencyKey);
|
|
205
291
|
}
|
|
206
|
-
requestBackgroundRemoval(assetId) {
|
|
207
|
-
return this.
|
|
292
|
+
requestBackgroundRemoval(assetId, idempotencyKey) {
|
|
293
|
+
return this.requestAssetJob(`/assets/${encodeURIComponent(assetId)}/background-remove`, idempotencyKey);
|
|
208
294
|
}
|
|
209
295
|
versions(assetId) {
|
|
210
296
|
return this.client.request(`/assets/${encodeURIComponent(assetId)}/versions`);
|
|
@@ -274,9 +360,81 @@ export class AssetsClient {
|
|
|
274
360
|
body: transformBody(options),
|
|
275
361
|
});
|
|
276
362
|
}
|
|
363
|
+
getAIConfig() {
|
|
364
|
+
return this.client.request('/assets/ai/config');
|
|
365
|
+
}
|
|
366
|
+
updateAIConfig(revision, enabledActions, monthlySpendCapMicros) {
|
|
367
|
+
return this.client.request('/assets/ai/config', {
|
|
368
|
+
method: 'PUT', headers: { 'If-Match': `"${revision}"` },
|
|
369
|
+
body: { enabled_actions: enabledActions, monthly_spend_cap_micros: monthlySpendCapMicros },
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
job(jobId) {
|
|
373
|
+
return this.client.request(`/assets/jobs/${encodeURIComponent(jobId)}`);
|
|
374
|
+
}
|
|
375
|
+
cancelJob(jobId) {
|
|
376
|
+
return this.client.request(`/assets/jobs/${encodeURIComponent(jobId)}/cancel`, { method: 'POST' });
|
|
377
|
+
}
|
|
378
|
+
createLifecycleRule(input) {
|
|
379
|
+
return this.client.request('/assets/lifecycle-rules', { method: 'POST', body: input });
|
|
380
|
+
}
|
|
381
|
+
listLifecycleRules() {
|
|
382
|
+
return this.client.request('/assets/lifecycle-rules');
|
|
383
|
+
}
|
|
384
|
+
deleteLifecycleRule(ruleId) {
|
|
385
|
+
return this.client.request(`/assets/lifecycle-rules/${encodeURIComponent(ruleId)}`, { method: 'DELETE' });
|
|
386
|
+
}
|
|
387
|
+
createDomain(domain) {
|
|
388
|
+
return this.client.request('/assets/domains', { method: 'POST', body: { domain } });
|
|
389
|
+
}
|
|
390
|
+
listDomains() {
|
|
391
|
+
return this.client.request('/assets/domains');
|
|
392
|
+
}
|
|
393
|
+
verifyDomain(domainId) {
|
|
394
|
+
return this.client.request(`/assets/domains/${encodeURIComponent(domainId)}/verify`, { method: 'POST' });
|
|
395
|
+
}
|
|
396
|
+
deleteDomain(domainId) {
|
|
397
|
+
return this.client.request(`/assets/domains/${encodeURIComponent(domainId)}`, { method: 'DELETE' });
|
|
398
|
+
}
|
|
399
|
+
analytics() {
|
|
400
|
+
return this.client.request('/assets/analytics');
|
|
401
|
+
}
|
|
402
|
+
usageSummary() {
|
|
403
|
+
return this.client.request('/assets/summary');
|
|
404
|
+
}
|
|
405
|
+
events(limit = 50) {
|
|
406
|
+
return this.client.request(`/assets/events?limit=${limit}`);
|
|
407
|
+
}
|
|
408
|
+
createWebhook(url, eventTypes = ['asset.*']) {
|
|
409
|
+
return this.client.request('/assets/webhooks', {
|
|
410
|
+
method: 'POST', body: { url, event_types: eventTypes },
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
listWebhooks() {
|
|
414
|
+
return this.client.request('/assets/webhooks');
|
|
415
|
+
}
|
|
416
|
+
deleteWebhook(webhookId) {
|
|
417
|
+
return this.client.request(`/assets/webhooks/${encodeURIComponent(webhookId)}`, { method: 'DELETE' });
|
|
418
|
+
}
|
|
419
|
+
listWebhookDeliveries(webhookId, limit = 50) {
|
|
420
|
+
return this.client.request(`/assets/webhooks/${encodeURIComponent(webhookId)}/deliveries?limit=${limit}`);
|
|
421
|
+
}
|
|
422
|
+
retryWebhookDelivery(deliveryId) {
|
|
423
|
+
return this.client.request(`/assets/webhook-deliveries/${encodeURIComponent(deliveryId)}/retry`, { method: 'POST' });
|
|
424
|
+
}
|
|
425
|
+
purge(assetId) {
|
|
426
|
+
return this.client.request(`/assets/${encodeURIComponent(assetId)}/purge`, { method: 'POST' });
|
|
427
|
+
}
|
|
428
|
+
bulk(input) {
|
|
429
|
+
const { idempotencyKey, ...body } = input;
|
|
430
|
+
return this.client.request('/assets/bulk', { method: 'POST', body, headers: idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : undefined });
|
|
431
|
+
}
|
|
277
432
|
originBaseUrl() {
|
|
278
433
|
return this.client.baseUrl.replace(/\/api\/v1$/, '');
|
|
279
434
|
}
|
|
435
|
+
requestAssetJob(path, idempotencyKey) {
|
|
436
|
+
return this.client.request(path, { method: 'POST', headers: idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : undefined });
|
|
437
|
+
}
|
|
280
438
|
async uploadSingleWithProgress(file, options, callbacks) {
|
|
281
439
|
const session = await this.createUploadSession({
|
|
282
440
|
...options,
|
|
@@ -294,7 +452,7 @@ function assetForm(file, options) {
|
|
|
294
452
|
append(body, 'key', options.key);
|
|
295
453
|
append(body, 'folder', options.folder);
|
|
296
454
|
append(body, 'visibility', options.visibility);
|
|
297
|
-
append(body, '
|
|
455
|
+
append(body, 'cache_control', options.cacheControl);
|
|
298
456
|
if (options.metadata) {
|
|
299
457
|
body.append('metadata', JSON.stringify(options.metadata));
|
|
300
458
|
}
|
|
@@ -314,6 +472,8 @@ function transformSpec(options) {
|
|
|
314
472
|
parts.push(`c_${options.crop ?? 'fit'}`);
|
|
315
473
|
parts.push(`f_${options.format ?? 'auto'}`);
|
|
316
474
|
parts.push(`q_${options.quality ?? 'auto'}`);
|
|
475
|
+
parts.push(`g_${options.gravity ?? 'centre'}`);
|
|
476
|
+
parts.push(`a_${options.animation ?? 'first'}`);
|
|
317
477
|
return parts.join(',');
|
|
318
478
|
}
|
|
319
479
|
function preset(name, category, description, width, height, crop = 'fit', format = 'auto', quality = 'auto') {
|
|
@@ -326,23 +486,33 @@ function transformBody(options) {
|
|
|
326
486
|
crop: options.crop,
|
|
327
487
|
format: options.format,
|
|
328
488
|
quality: options.quality,
|
|
329
|
-
|
|
489
|
+
gravity: options.gravity,
|
|
490
|
+
animation: options.animation,
|
|
491
|
+
expires_in: options.expiresIn,
|
|
330
492
|
};
|
|
331
493
|
}
|
|
332
494
|
const defaultPartSize = 5 * 1024 * 1024;
|
|
333
495
|
function blobName(file) {
|
|
334
496
|
return file.name;
|
|
335
497
|
}
|
|
336
|
-
function uploadBlob(url, blob, callbacks, partNumber) {
|
|
498
|
+
function uploadBlob(url, blob, callbacks, partNumber, headers = {}) {
|
|
337
499
|
if (typeof XMLHttpRequest === 'undefined') {
|
|
338
|
-
return fetch(url, { method: 'PUT', body: blob }).then((response) => {
|
|
500
|
+
return fetch(url, { method: 'PUT', body: blob, signal: callbacks.signal, headers }).then((response) => {
|
|
339
501
|
if (!response.ok)
|
|
340
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);
|
|
341
507
|
});
|
|
342
508
|
}
|
|
343
509
|
return new Promise((resolve, reject) => {
|
|
344
510
|
const xhr = new XMLHttpRequest();
|
|
345
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 });
|
|
346
516
|
xhr.upload.onprogress = (event) => {
|
|
347
517
|
const total = event.total || blob.size;
|
|
348
518
|
const progress = { loaded: event.loaded, total, percent: total ? Math.round((event.loaded / total) * 100) : 0 };
|
|
@@ -350,13 +520,64 @@ function uploadBlob(url, blob, callbacks, partNumber) {
|
|
|
350
520
|
if (partNumber)
|
|
351
521
|
callbacks.onPartProgress?.(partNumber, progress);
|
|
352
522
|
};
|
|
523
|
+
const cleanup = () => callbacks.signal?.removeEventListener('abort', abort);
|
|
353
524
|
xhr.onload = () => {
|
|
525
|
+
cleanup();
|
|
354
526
|
if (xhr.status >= 200 && xhr.status < 300)
|
|
355
527
|
resolve();
|
|
356
528
|
else
|
|
357
529
|
reject(new Error(`Upload failed with ${xhr.status}`));
|
|
358
530
|
};
|
|
359
|
-
xhr.onerror = () => reject(new Error('Upload failed'));
|
|
531
|
+
xhr.onerror = () => { cleanup(); reject(new Error('Upload failed')); };
|
|
532
|
+
xhr.onabort = () => { cleanup(); reject(abortError()); };
|
|
360
533
|
xhr.send(blob);
|
|
361
534
|
});
|
|
362
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
|
+
function throwIfAborted(signal) {
|
|
575
|
+
if (signal?.aborted)
|
|
576
|
+
throw abortError();
|
|
577
|
+
}
|
|
578
|
+
function abortError() {
|
|
579
|
+
return typeof DOMException === 'undefined' ? Object.assign(new Error('Upload cancelled'), { name: 'AbortError' }) : new DOMException('Upload cancelled', 'AbortError');
|
|
580
|
+
}
|
|
581
|
+
function isAbortError(error) {
|
|
582
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
583
|
+
}
|