transloadit 4.12.0 → 4.13.1
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 +232 -23
- package/dist/Transloadit.d.ts +55 -1
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +143 -2
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/types/assemblyStatus.d.ts +574 -198
- package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyStatus.js +17 -0
- package/dist/alphalib/types/assemblyStatus.js.map +1 -1
- package/dist/alphalib/types/robots/_index.d.ts +205 -93
- package/dist/alphalib/types/robots/_index.d.ts.map +1 -1
- package/dist/alphalib/types/robots/_index.js +8 -0
- package/dist/alphalib/types/robots/_index.js.map +1 -1
- package/dist/alphalib/types/robots/_instructions-primitives.d.ts +4 -4
- package/dist/alphalib/types/robots/_instructions-primitives.d.ts.map +1 -1
- package/dist/alphalib/types/robots/_instructions-primitives.js +2 -0
- package/dist/alphalib/types/robots/_instructions-primitives.js.map +1 -1
- package/dist/alphalib/types/robots/file-serve.d.ts +12 -0
- package/dist/alphalib/types/robots/file-serve.d.ts.map +1 -1
- package/dist/alphalib/types/robots/file-serve.js +9 -3
- package/dist/alphalib/types/robots/file-serve.js.map +1 -1
- package/dist/alphalib/types/robots/transloadit-import.d.ts +195 -0
- package/dist/alphalib/types/robots/transloadit-import.d.ts.map +1 -0
- package/dist/alphalib/types/robots/transloadit-import.js +108 -0
- package/dist/alphalib/types/robots/transloadit-import.js.map +1 -0
- package/dist/alphalib/types/robots/transloadit-store.d.ts +441 -0
- package/dist/alphalib/types/robots/transloadit-store.d.ts.map +1 -0
- package/dist/alphalib/types/robots/transloadit-store.js +69 -0
- package/dist/alphalib/types/robots/transloadit-store.js.map +1 -0
- package/dist/alphalib/types/robots/video-ondemand.d.ts +8 -8
- package/dist/alphalib/types/storageAsset.d.ts +226 -0
- package/dist/alphalib/types/storageAsset.d.ts.map +1 -0
- package/dist/alphalib/types/storageAsset.js +67 -0
- package/dist/alphalib/types/storageAsset.js.map +1 -0
- package/dist/alphalib/types/template.d.ts.map +1 -1
- package/dist/alphalib/types/template.js +5 -2
- package/dist/alphalib/types/template.js.map +1 -1
- package/dist/cli/commands/BaseCommand.d.ts +3 -1
- package/dist/cli/commands/BaseCommand.d.ts.map +1 -1
- package/dist/cli/commands/BaseCommand.js +3 -2
- package/dist/cli/commands/BaseCommand.js.map +1 -1
- package/dist/cli/commands/assemblies.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.js +9 -4
- package/dist/cli/commands/assemblies.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +6 -0
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +21 -2
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/image.d.ts +15 -0
- package/dist/cli/commands/image.d.ts.map +1 -0
- package/dist/cli/commands/image.js +234 -0
- package/dist/cli/commands/image.js.map +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +15 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/login.d.ts +24 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +275 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/storage.d.ts +58 -0
- package/dist/cli/commands/storage.d.ts.map +1 -0
- package/dist/cli/commands/storage.js +589 -0
- package/dist/cli/commands/storage.js.map +1 -0
- package/dist/cli/commands/templates.d.ts.map +1 -1
- package/dist/cli/commands/templates.js +10 -6
- package/dist/cli/commands/templates.js.map +1 -1
- package/dist/cli/deviceLogin.d.ts +11 -0
- package/dist/cli/deviceLogin.d.ts.map +1 -0
- package/dist/cli/deviceLogin.js +187 -0
- package/dist/cli/deviceLogin.js.map +1 -0
- package/dist/cli/helpers.d.ts +23 -1
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js +159 -25
- package/dist/cli/helpers.js.map +1 -1
- package/dist/cli/secretInput.d.ts +3 -0
- package/dist/cli/secretInput.d.ts.map +1 -0
- package/dist/cli/secretInput.js +11 -0
- package/dist/cli/secretInput.js.map +1 -0
- package/dist/cli/storageCatalog.d.ts +19 -0
- package/dist/cli/storageCatalog.d.ts.map +1 -0
- package/dist/cli/storageCatalog.js +129 -0
- package/dist/cli/storageCatalog.js.map +1 -0
- package/dist/cli/storagePublic.d.ts +3 -0
- package/dist/cli/storagePublic.d.ts.map +1 -0
- package/dist/cli/storagePublic.js +15 -0
- package/dist/cli/storagePublic.js.map +1 -0
- package/dist/cli/storageReceipts.d.ts +67 -0
- package/dist/cli/storageReceipts.d.ts.map +1 -0
- package/dist/cli/storageReceipts.js +242 -0
- package/dist/cli/storageReceipts.js.map +1 -0
- package/dist/cli/storageSnippets.d.ts +11 -0
- package/dist/cli/storageSnippets.d.ts.map +1 -0
- package/dist/cli/storageSnippets.js +89 -0
- package/dist/cli/storageSnippets.js.map +1 -0
- package/dist/storageImage.d.ts +40 -0
- package/dist/storageImage.d.ts.map +1 -0
- package/dist/storageImage.js +139 -0
- package/dist/storageImage.js.map +1 -0
- package/dist/storagePublicPrefixes.d.ts +62 -0
- package/dist/storagePublicPrefixes.d.ts.map +1 -0
- package/dist/storagePublicPrefixes.js +35 -0
- package/dist/storagePublicPrefixes.js.map +1 -0
- package/dist/storageResults.d.ts +31 -0
- package/dist/storageResults.d.ts.map +1 -0
- package/dist/storageResults.js +79 -0
- package/dist/storageResults.js.map +1 -0
- package/package.json +4 -3
- package/src/Transloadit.ts +251 -4
- package/src/alphalib/types/assemblyStatus.ts +17 -0
- package/src/alphalib/types/robots/_index.ts +28 -0
- package/src/alphalib/types/robots/_instructions-primitives.ts +2 -0
- package/src/alphalib/types/robots/file-serve.ts +11 -3
- package/src/alphalib/types/robots/transloadit-import.ts +154 -0
- package/src/alphalib/types/robots/transloadit-store.ts +98 -0
- package/src/alphalib/types/storageAsset.ts +88 -0
- package/src/alphalib/types/template.ts +3 -0
- package/src/cli/commands/BaseCommand.ts +4 -2
- package/src/cli/commands/assemblies.ts +13 -5
- package/src/cli/commands/auth.ts +26 -3
- package/src/cli/commands/image.ts +282 -0
- package/src/cli/commands/index.ts +27 -1
- package/src/cli/commands/login.ts +334 -0
- package/src/cli/commands/storage.ts +733 -0
- package/src/cli/commands/templates.ts +14 -7
- package/src/cli/deviceLogin.ts +226 -0
- package/src/cli/helpers.ts +204 -27
- package/src/cli/secretInput.ts +12 -0
- package/src/cli/storageCatalog.ts +192 -0
- package/src/cli/storagePublic.ts +15 -0
- package/src/cli/storageReceipts.ts +307 -0
- package/src/cli/storageSnippets.ts +96 -0
- package/src/storageImage.ts +222 -0
- package/src/storagePublicPrefixes.ts +39 -0
- package/src/storageResults.ts +119 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { AssemblyStatus } from './alphalib/types/assemblyStatus.ts'
|
|
2
|
+
import type { StoredAsset } from './alphalib/types/storageAsset.ts'
|
|
3
|
+
import type { CreateAssemblyOptions, Transloadit } from './Transloadit.ts'
|
|
4
|
+
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
import { createReadStream } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
import debug from 'debug'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
|
|
11
|
+
import { ApiError } from './ApiError.ts'
|
|
12
|
+
import { storedAssetSchema } from './alphalib/types/storageAsset.ts'
|
|
13
|
+
import InconsistentResponseError from './InconsistentResponseError.ts'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Verified Storage metadata that can be saved and passed directly to an image renderer.
|
|
17
|
+
* Width and height reflect EXIF auto-orientation, matching Storage preview delivery.
|
|
18
|
+
*/
|
|
19
|
+
export interface StoredImageReceipt extends Readonly<StoredAsset> {
|
|
20
|
+
/** Optional metadata indicating that the original has an alpha channel. */
|
|
21
|
+
readonly hasAlpha?: boolean
|
|
22
|
+
readonly height: number
|
|
23
|
+
readonly md5hash: string
|
|
24
|
+
/** Optional base64 ThumbHash of the original pixels, for an inline blur placeholder. */
|
|
25
|
+
readonly thumbhash?: string
|
|
26
|
+
readonly width: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One explicit destination, with the existing Assembly upload and polling controls. */
|
|
30
|
+
export interface StoreImageOptions
|
|
31
|
+
extends Pick<
|
|
32
|
+
CreateAssemblyOptions,
|
|
33
|
+
'chunkSize' | 'onAssemblyProgress' | 'onUploadProgress' | 'signal' | 'timeout'
|
|
34
|
+
> {
|
|
35
|
+
/** Complete relative Storage filename. Directories and interpolation expressions are rejected. */
|
|
36
|
+
path: string
|
|
37
|
+
/** Explicit opt-in replacement of an existing path; defaults to false. */
|
|
38
|
+
overwrite?: boolean
|
|
39
|
+
/** Observe verified metadata. Not awaited; sync and async observer errors cannot undo a write. */
|
|
40
|
+
onReceipt?: (
|
|
41
|
+
receipt: StoredImageReceipt,
|
|
42
|
+
input: StoredImageExpectation,
|
|
43
|
+
assemblyId: string | undefined,
|
|
44
|
+
) => void | Promise<void>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Trusted upload facts used to correlate a stored result with an application-owned upload. */
|
|
48
|
+
export interface StoredImageExpectation {
|
|
49
|
+
path: string
|
|
50
|
+
size: number
|
|
51
|
+
md5hash: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Recovers one original's receipt without re-uploading or trusting notification payloads. */
|
|
55
|
+
export interface GetStoredImageReceiptOptions {
|
|
56
|
+
assemblyId: string
|
|
57
|
+
expected: StoredImageExpectation
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const positiveIntegerSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
|
61
|
+
const receiptRequestSchema = z.object({
|
|
62
|
+
assemblyId: z.string().regex(/^[A-Za-z0-9_-]+$/),
|
|
63
|
+
expected: z.object({
|
|
64
|
+
path: z.string(),
|
|
65
|
+
size: positiveIntegerSchema,
|
|
66
|
+
md5hash: z.string().regex(/^[a-f0-9]{32}$/),
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
const completedImageSchema = z.object({
|
|
70
|
+
ok: z.literal('ASSEMBLY_COMPLETED'),
|
|
71
|
+
results: z.object({
|
|
72
|
+
':original': z.tuple([
|
|
73
|
+
storedAssetSchema.extend({
|
|
74
|
+
md5hash: storedAssetSchema.shape.md5hash.unwrap(),
|
|
75
|
+
height: positiveIntegerSchema,
|
|
76
|
+
width: positiveIntegerSchema,
|
|
77
|
+
size: positiveIntegerSchema,
|
|
78
|
+
}),
|
|
79
|
+
]),
|
|
80
|
+
}),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
function validateDestination(path: string): void {
|
|
84
|
+
if (
|
|
85
|
+
typeof path !== 'string' ||
|
|
86
|
+
path.trim() !== path ||
|
|
87
|
+
Buffer.byteLength(path) > 1024 ||
|
|
88
|
+
path.normalize('NFC') !== path ||
|
|
89
|
+
/[\p{Cc}\p{Cs}\\|]/u.test(path) ||
|
|
90
|
+
path.includes('${') ||
|
|
91
|
+
path.split('/').some((segment) => segment.trim() === '' || segment === '.' || segment === '..')
|
|
92
|
+
) {
|
|
93
|
+
throw new TypeError('Storage path must be a complete relative filename without interpolation')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Stores one original without overwriting; receipt validation occurs after the Storage write. */
|
|
98
|
+
export async function storeImage(
|
|
99
|
+
client: Transloadit,
|
|
100
|
+
filePath: string,
|
|
101
|
+
options: StoreImageOptions,
|
|
102
|
+
): Promise<StoredImageReceipt> {
|
|
103
|
+
const {
|
|
104
|
+
path,
|
|
105
|
+
chunkSize,
|
|
106
|
+
onAssemblyProgress,
|
|
107
|
+
onUploadProgress,
|
|
108
|
+
onReceipt,
|
|
109
|
+
overwrite,
|
|
110
|
+
signal,
|
|
111
|
+
timeout,
|
|
112
|
+
} = options
|
|
113
|
+
validateDestination(path)
|
|
114
|
+
if (overwrite !== undefined && typeof overwrite !== 'boolean')
|
|
115
|
+
throw new TypeError('overwrite must be a boolean')
|
|
116
|
+
signal?.throwIfAborted()
|
|
117
|
+
const checksum = createHash('md5')
|
|
118
|
+
let size = 0
|
|
119
|
+
for await (const chunk of createReadStream(filePath, { signal })) {
|
|
120
|
+
checksum.update(chunk)
|
|
121
|
+
size += chunk.length
|
|
122
|
+
}
|
|
123
|
+
if (size === 0) throw new Error('Cannot store an empty image')
|
|
124
|
+
const md5hash = checksum.digest('hex')
|
|
125
|
+
signal?.throwIfAborted()
|
|
126
|
+
const assembly = await client.createAssembly({
|
|
127
|
+
chunkSize,
|
|
128
|
+
files: { image: filePath },
|
|
129
|
+
onAssemblyProgress,
|
|
130
|
+
onUploadProgress,
|
|
131
|
+
params: {
|
|
132
|
+
steps: {
|
|
133
|
+
stored: {
|
|
134
|
+
robot: '/transloadit/store',
|
|
135
|
+
use: ':original',
|
|
136
|
+
path,
|
|
137
|
+
conflict_strategy: overwrite === true ? 'overwrite' : 'error',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
signal,
|
|
142
|
+
timeout,
|
|
143
|
+
waitForCompletion: true,
|
|
144
|
+
})
|
|
145
|
+
const input = { path, size, md5hash }
|
|
146
|
+
// API2 can watermark uploads before Robots run; this exact write's result describes stored bytes.
|
|
147
|
+
const receipt = validateReceipt(assembly, input, true)
|
|
148
|
+
const observerFailed = (): void => {
|
|
149
|
+
debug('transloadit:warn')('Ignored onReceipt observer failure after a completed Storage write')
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
// Neither a stalled observer nor its rejection may hide a successfully stored receipt.
|
|
153
|
+
void Promise.resolve(onReceipt?.({ ...receipt }, input, assembly.assembly_id)).catch(
|
|
154
|
+
observerFailed,
|
|
155
|
+
)
|
|
156
|
+
} catch {
|
|
157
|
+
observerFailed()
|
|
158
|
+
}
|
|
159
|
+
return receipt
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Fetches authoritative Assembly status and applies the same validation as a local image store. */
|
|
163
|
+
export async function getStoredImageReceipt(
|
|
164
|
+
client: Transloadit,
|
|
165
|
+
options: GetStoredImageReceiptOptions,
|
|
166
|
+
): Promise<StoredImageReceipt> {
|
|
167
|
+
const { assemblyId, expected } = receiptRequestSchema.parse(options)
|
|
168
|
+
validateDestination(expected.path)
|
|
169
|
+
const assembly = await client.getAssembly(assemblyId)
|
|
170
|
+
if (assembly.assembly_id !== assemblyId) {
|
|
171
|
+
throw new InconsistentResponseError('The response did not match the requested Assembly', {
|
|
172
|
+
cause: { assemblyId },
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
return validateReceipt(assembly, expected)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateReceipt(
|
|
179
|
+
assembly: AssemblyStatus,
|
|
180
|
+
expected: StoredImageExpectation,
|
|
181
|
+
acceptTransformed = false,
|
|
182
|
+
): StoredImageReceipt {
|
|
183
|
+
const { path, size, md5hash } = expected
|
|
184
|
+
if (typeof assembly.error === 'string') throw new ApiError({ body: assembly })
|
|
185
|
+
if (assembly.ok === 'ASSEMBLY_CANCELED') {
|
|
186
|
+
throw new InconsistentResponseError('The Storage Assembly ended with ASSEMBLY_CANCELED', {
|
|
187
|
+
cause: { assemblyId: assembly.assembly_id },
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
assembly.ok === 'ASSEMBLY_UPLOADING' ||
|
|
192
|
+
assembly.ok === 'ASSEMBLY_EXECUTING' ||
|
|
193
|
+
assembly.ok === 'ASSEMBLY_REPLAYING'
|
|
194
|
+
) {
|
|
195
|
+
throw new InconsistentResponseError(`The Storage Assembly is not complete (${assembly.ok})`, {
|
|
196
|
+
cause: { assemblyId: assembly.assembly_id },
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
const parsed = completedImageSchema.safeParse(assembly)
|
|
200
|
+
const result = parsed.success ? parsed.data.results[':original'][0] : undefined
|
|
201
|
+
const originals = assembly.results?.[':original']
|
|
202
|
+
const receiptCheck = {
|
|
203
|
+
originalCount: Array.isArray(originals) ? originals.length : 0,
|
|
204
|
+
metadataValid: parsed.success,
|
|
205
|
+
pathMatches: result === undefined ? undefined : result.path === path,
|
|
206
|
+
sizeMatches: result === undefined ? undefined : result.size === size,
|
|
207
|
+
md5Matches: result === undefined ? undefined : result.md5hash === md5hash,
|
|
208
|
+
}
|
|
209
|
+
if (
|
|
210
|
+
result === undefined ||
|
|
211
|
+
result.path !== path ||
|
|
212
|
+
(!acceptTransformed && (!receiptCheck.sizeMatches || !receiptCheck.md5Matches))
|
|
213
|
+
) {
|
|
214
|
+
throw new InconsistentResponseError(
|
|
215
|
+
'The Assembly did not return a matching Storage image receipt',
|
|
216
|
+
{
|
|
217
|
+
cause: { assemblyId: assembly.assembly_id, receiptCheck },
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
return result
|
|
222
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { validateStoragePathPrefix } from '@transloadit/utils'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
/** Normalize a public directory, never implicit workspace-wide access. */
|
|
5
|
+
export function normalizeStoragePublicPrefix(prefix: string): string {
|
|
6
|
+
if (typeof prefix !== 'string' || prefix === '' || prefix === '/')
|
|
7
|
+
throw new TypeError('A public prefix must name a non-root directory')
|
|
8
|
+
const normalized = prefix.endsWith('/') ? prefix : `${prefix}/`
|
|
9
|
+
validateStoragePathPrefix(normalized, 0, 'public prefix')
|
|
10
|
+
if (new TextEncoder().encode(normalized).byteLength > 512)
|
|
11
|
+
throw new TypeError('A public prefix must be at most 512 UTF-8 bytes')
|
|
12
|
+
return normalized
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const prefixSchema = z.string().refine((prefix) => {
|
|
16
|
+
try {
|
|
17
|
+
return normalizeStoragePublicPrefix(prefix) === prefix
|
|
18
|
+
} catch {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}, 'Expected a normalized public directory')
|
|
22
|
+
const publicPrefixSchema = z.object({ prefix: prefixSchema, created_at: z.string() })
|
|
23
|
+
export const storagePublicPrefixesSchema = z.object({
|
|
24
|
+
ok: z.literal('STORAGE_PUBLIC_PREFIXES_LISTED'),
|
|
25
|
+
public_prefixes: z.array(publicPrefixSchema),
|
|
26
|
+
})
|
|
27
|
+
export const storagePublicPrefixDeclaredSchema = publicPrefixSchema.extend({
|
|
28
|
+
ok: z.literal('STORAGE_PUBLIC_PREFIX_DECLARED'),
|
|
29
|
+
created: z.boolean(),
|
|
30
|
+
})
|
|
31
|
+
export const storagePublicPrefixRevokedSchema = z.object({
|
|
32
|
+
ok: z.literal('STORAGE_PUBLIC_PREFIX_REVOKED'),
|
|
33
|
+
prefix: prefixSchema,
|
|
34
|
+
deleted: z.boolean(),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export type StoragePublicPrefixes = z.infer<typeof storagePublicPrefixesSchema>
|
|
38
|
+
export type StoragePublicPrefixDeclared = z.infer<typeof storagePublicPrefixDeclaredSchema>
|
|
39
|
+
export type StoragePublicPrefixRevoked = z.infer<typeof storagePublicPrefixRevokedSchema>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { StoredAsset } from './alphalib/types/storageAsset.ts'
|
|
2
|
+
import type { Transloadit } from './Transloadit.ts'
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
import { ApiError } from './ApiError.ts'
|
|
7
|
+
import { robotFileServeInstructionsSchema } from './alphalib/types/robots/file-serve.ts'
|
|
8
|
+
import { storedAssetSchema } from './alphalib/types/storageAsset.ts'
|
|
9
|
+
import InconsistentResponseError from './InconsistentResponseError.ts'
|
|
10
|
+
|
|
11
|
+
/** Original-byte delivery, not a preview or transcode; authorize the application's asset first. */
|
|
12
|
+
export interface StoredAssetUrlOptions {
|
|
13
|
+
/** True uses the receipt filename; a string supplies a different attachment filename. */
|
|
14
|
+
download?: boolean | string
|
|
15
|
+
/** Maximum signed lifetime, from one second to 48 hours. Defaults to five minutes. */
|
|
16
|
+
lifetimeMs?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Signs a retained original, including non-image media, without fetching or proxying its bytes. */
|
|
20
|
+
export function getStoredAssetUrl(
|
|
21
|
+
client: Transloadit,
|
|
22
|
+
input: StoredAsset,
|
|
23
|
+
options: StoredAssetUrlOptions = {},
|
|
24
|
+
): string {
|
|
25
|
+
const asset = storedAssetSchema.parse(input)
|
|
26
|
+
const lifetimeMs = options.lifetimeMs ?? 5 * 60_000
|
|
27
|
+
if (!Number.isSafeInteger(lifetimeMs) || lifetimeMs < 1000 || lifetimeMs > 48 * 3600_000) {
|
|
28
|
+
throw new RangeError('lifetimeMs must be an integer between 1000 and 172800000')
|
|
29
|
+
}
|
|
30
|
+
const download = options.download === true ? asset.path.split('/').at(-1) : options.download
|
|
31
|
+
if (download !== undefined && download !== false) {
|
|
32
|
+
robotFileServeInstructionsSchema.shape.download_name.unwrap().min(1).parse(download)
|
|
33
|
+
if (typeof download !== 'string' || /[\uD800-\uDFFF]/u.test(download))
|
|
34
|
+
throw new TypeError('download must contain a valid Unicode filename')
|
|
35
|
+
}
|
|
36
|
+
// Bunny includes the full query in its cache key. Rotate at most once a minute, never at
|
|
37
|
+
// the lifetime boundary where rounding could issue an almost-expired grant.
|
|
38
|
+
const rotationMs = Math.min(60_000, Math.floor(lifetimeMs / 2))
|
|
39
|
+
const expiresAt = Math.floor(Date.now() / rotationMs) * rotationMs + lifetimeMs
|
|
40
|
+
return client.getSignedSmartCDNUrl({
|
|
41
|
+
workspace: asset.workspace,
|
|
42
|
+
template: 'builtin/storage-serve@0.0.3',
|
|
43
|
+
input: asset.asset_id,
|
|
44
|
+
urlParams: { v: asset.version_id, ...(typeof download === 'string' ? { download } : {}) },
|
|
45
|
+
expiresAt,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Server-owned Assembly identity and expected Workspace, not values from an untrusted notification. */
|
|
50
|
+
export interface GetStoredAssemblyResultsOptions {
|
|
51
|
+
assemblyId: string
|
|
52
|
+
workspace: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A canonical stored asset with the producing result's provenance; one input can have many outputs. */
|
|
56
|
+
export interface StoredAssemblyResult {
|
|
57
|
+
assembly_id: string
|
|
58
|
+
step: string
|
|
59
|
+
result_id: string
|
|
60
|
+
original_id?: string | (string | null)[]
|
|
61
|
+
asset: StoredAsset
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const requestSchema = z.object({
|
|
65
|
+
assemblyId: z.string().regex(/^[A-Za-z0-9_-]+$/),
|
|
66
|
+
workspace: z.string().min(1),
|
|
67
|
+
})
|
|
68
|
+
const resultSchema = storedAssetSchema.extend({
|
|
69
|
+
id: z.string().min(1),
|
|
70
|
+
original_id: z.union([z.string(), z.array(z.string().nullable())]).optional(),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fetches a completed Assembly and verifies all retained results, including non-image media.
|
|
75
|
+
* The caller must first bind the Assembly to an authorized application upload. Register results
|
|
76
|
+
* idempotently by Assembly/step/result ID (or asset/version), not by filename or notification count.
|
|
77
|
+
*/
|
|
78
|
+
export async function getStoredAssemblyResults(
|
|
79
|
+
client: Transloadit,
|
|
80
|
+
options: GetStoredAssemblyResultsOptions,
|
|
81
|
+
): Promise<StoredAssemblyResult[]> {
|
|
82
|
+
const { assemblyId, workspace } = requestSchema.parse(options)
|
|
83
|
+
const assembly = await client.getAssembly(assemblyId)
|
|
84
|
+
if (assembly.assembly_id !== assemblyId) {
|
|
85
|
+
throw new InconsistentResponseError('The response did not match the requested Assembly')
|
|
86
|
+
}
|
|
87
|
+
if (typeof assembly.error === 'string') throw new ApiError({ body: assembly })
|
|
88
|
+
if (assembly.ok !== 'ASSEMBLY_COMPLETED') {
|
|
89
|
+
throw new InconsistentResponseError(`The Storage Assembly is not complete (${assembly.ok})`)
|
|
90
|
+
}
|
|
91
|
+
if (assembly.results === undefined) {
|
|
92
|
+
throw new InconsistentResponseError('The completed Assembly did not contain results')
|
|
93
|
+
}
|
|
94
|
+
const results: StoredAssemblyResult[] = []
|
|
95
|
+
for (const [step, files] of Object.entries(assembly.results)) {
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
// Ordinary temporary/exported outputs are not Storage receipts. Partial Storage records
|
|
98
|
+
// must fail validation rather than disappearing from an otherwise successful batch.
|
|
99
|
+
if (
|
|
100
|
+
file.asset_id === undefined &&
|
|
101
|
+
file.version_id === undefined &&
|
|
102
|
+
file.workspace === undefined
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
const parsed = resultSchema.safeParse(file)
|
|
106
|
+
if (!parsed.success || parsed.data.workspace !== workspace) {
|
|
107
|
+
throw new InconsistentResponseError(
|
|
108
|
+
'The Assembly returned an invalid or cross-Workspace Storage result',
|
|
109
|
+
{
|
|
110
|
+
cause: { assemblyId, step },
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
const { id, original_id, ...asset } = parsed.data
|
|
115
|
+
results.push({ assembly_id: assemblyId, step, result_id: id, original_id, asset })
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return results
|
|
119
|
+
}
|