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.
Files changed (134) hide show
  1. package/README.md +232 -23
  2. package/dist/Transloadit.d.ts +55 -1
  3. package/dist/Transloadit.d.ts.map +1 -1
  4. package/dist/Transloadit.js +143 -2
  5. package/dist/Transloadit.js.map +1 -1
  6. package/dist/alphalib/types/assemblyStatus.d.ts +574 -198
  7. package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
  8. package/dist/alphalib/types/assemblyStatus.js +17 -0
  9. package/dist/alphalib/types/assemblyStatus.js.map +1 -1
  10. package/dist/alphalib/types/robots/_index.d.ts +205 -93
  11. package/dist/alphalib/types/robots/_index.d.ts.map +1 -1
  12. package/dist/alphalib/types/robots/_index.js +8 -0
  13. package/dist/alphalib/types/robots/_index.js.map +1 -1
  14. package/dist/alphalib/types/robots/_instructions-primitives.d.ts +4 -4
  15. package/dist/alphalib/types/robots/_instructions-primitives.d.ts.map +1 -1
  16. package/dist/alphalib/types/robots/_instructions-primitives.js +2 -0
  17. package/dist/alphalib/types/robots/_instructions-primitives.js.map +1 -1
  18. package/dist/alphalib/types/robots/file-serve.d.ts +12 -0
  19. package/dist/alphalib/types/robots/file-serve.d.ts.map +1 -1
  20. package/dist/alphalib/types/robots/file-serve.js +9 -3
  21. package/dist/alphalib/types/robots/file-serve.js.map +1 -1
  22. package/dist/alphalib/types/robots/transloadit-import.d.ts +195 -0
  23. package/dist/alphalib/types/robots/transloadit-import.d.ts.map +1 -0
  24. package/dist/alphalib/types/robots/transloadit-import.js +108 -0
  25. package/dist/alphalib/types/robots/transloadit-import.js.map +1 -0
  26. package/dist/alphalib/types/robots/transloadit-store.d.ts +441 -0
  27. package/dist/alphalib/types/robots/transloadit-store.d.ts.map +1 -0
  28. package/dist/alphalib/types/robots/transloadit-store.js +69 -0
  29. package/dist/alphalib/types/robots/transloadit-store.js.map +1 -0
  30. package/dist/alphalib/types/robots/video-ondemand.d.ts +8 -8
  31. package/dist/alphalib/types/storageAsset.d.ts +226 -0
  32. package/dist/alphalib/types/storageAsset.d.ts.map +1 -0
  33. package/dist/alphalib/types/storageAsset.js +67 -0
  34. package/dist/alphalib/types/storageAsset.js.map +1 -0
  35. package/dist/alphalib/types/template.d.ts.map +1 -1
  36. package/dist/alphalib/types/template.js +5 -2
  37. package/dist/alphalib/types/template.js.map +1 -1
  38. package/dist/cli/commands/BaseCommand.d.ts +3 -1
  39. package/dist/cli/commands/BaseCommand.d.ts.map +1 -1
  40. package/dist/cli/commands/BaseCommand.js +3 -2
  41. package/dist/cli/commands/BaseCommand.js.map +1 -1
  42. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  43. package/dist/cli/commands/assemblies.js +9 -4
  44. package/dist/cli/commands/assemblies.js.map +1 -1
  45. package/dist/cli/commands/auth.d.ts +6 -0
  46. package/dist/cli/commands/auth.d.ts.map +1 -1
  47. package/dist/cli/commands/auth.js +21 -2
  48. package/dist/cli/commands/auth.js.map +1 -1
  49. package/dist/cli/commands/image.d.ts +15 -0
  50. package/dist/cli/commands/image.d.ts.map +1 -0
  51. package/dist/cli/commands/image.js +234 -0
  52. package/dist/cli/commands/image.js.map +1 -0
  53. package/dist/cli/commands/index.d.ts.map +1 -1
  54. package/dist/cli/commands/index.js +15 -1
  55. package/dist/cli/commands/index.js.map +1 -1
  56. package/dist/cli/commands/login.d.ts +24 -0
  57. package/dist/cli/commands/login.d.ts.map +1 -0
  58. package/dist/cli/commands/login.js +275 -0
  59. package/dist/cli/commands/login.js.map +1 -0
  60. package/dist/cli/commands/storage.d.ts +58 -0
  61. package/dist/cli/commands/storage.d.ts.map +1 -0
  62. package/dist/cli/commands/storage.js +589 -0
  63. package/dist/cli/commands/storage.js.map +1 -0
  64. package/dist/cli/commands/templates.d.ts.map +1 -1
  65. package/dist/cli/commands/templates.js +10 -6
  66. package/dist/cli/commands/templates.js.map +1 -1
  67. package/dist/cli/deviceLogin.d.ts +11 -0
  68. package/dist/cli/deviceLogin.d.ts.map +1 -0
  69. package/dist/cli/deviceLogin.js +187 -0
  70. package/dist/cli/deviceLogin.js.map +1 -0
  71. package/dist/cli/helpers.d.ts +23 -1
  72. package/dist/cli/helpers.d.ts.map +1 -1
  73. package/dist/cli/helpers.js +159 -25
  74. package/dist/cli/helpers.js.map +1 -1
  75. package/dist/cli/secretInput.d.ts +3 -0
  76. package/dist/cli/secretInput.d.ts.map +1 -0
  77. package/dist/cli/secretInput.js +11 -0
  78. package/dist/cli/secretInput.js.map +1 -0
  79. package/dist/cli/storageCatalog.d.ts +19 -0
  80. package/dist/cli/storageCatalog.d.ts.map +1 -0
  81. package/dist/cli/storageCatalog.js +129 -0
  82. package/dist/cli/storageCatalog.js.map +1 -0
  83. package/dist/cli/storagePublic.d.ts +3 -0
  84. package/dist/cli/storagePublic.d.ts.map +1 -0
  85. package/dist/cli/storagePublic.js +15 -0
  86. package/dist/cli/storagePublic.js.map +1 -0
  87. package/dist/cli/storageReceipts.d.ts +67 -0
  88. package/dist/cli/storageReceipts.d.ts.map +1 -0
  89. package/dist/cli/storageReceipts.js +242 -0
  90. package/dist/cli/storageReceipts.js.map +1 -0
  91. package/dist/cli/storageSnippets.d.ts +11 -0
  92. package/dist/cli/storageSnippets.d.ts.map +1 -0
  93. package/dist/cli/storageSnippets.js +89 -0
  94. package/dist/cli/storageSnippets.js.map +1 -0
  95. package/dist/storageImage.d.ts +40 -0
  96. package/dist/storageImage.d.ts.map +1 -0
  97. package/dist/storageImage.js +139 -0
  98. package/dist/storageImage.js.map +1 -0
  99. package/dist/storagePublicPrefixes.d.ts +62 -0
  100. package/dist/storagePublicPrefixes.d.ts.map +1 -0
  101. package/dist/storagePublicPrefixes.js +35 -0
  102. package/dist/storagePublicPrefixes.js.map +1 -0
  103. package/dist/storageResults.d.ts +31 -0
  104. package/dist/storageResults.d.ts.map +1 -0
  105. package/dist/storageResults.js +79 -0
  106. package/dist/storageResults.js.map +1 -0
  107. package/package.json +4 -3
  108. package/src/Transloadit.ts +251 -4
  109. package/src/alphalib/types/assemblyStatus.ts +17 -0
  110. package/src/alphalib/types/robots/_index.ts +28 -0
  111. package/src/alphalib/types/robots/_instructions-primitives.ts +2 -0
  112. package/src/alphalib/types/robots/file-serve.ts +11 -3
  113. package/src/alphalib/types/robots/transloadit-import.ts +154 -0
  114. package/src/alphalib/types/robots/transloadit-store.ts +98 -0
  115. package/src/alphalib/types/storageAsset.ts +88 -0
  116. package/src/alphalib/types/template.ts +3 -0
  117. package/src/cli/commands/BaseCommand.ts +4 -2
  118. package/src/cli/commands/assemblies.ts +13 -5
  119. package/src/cli/commands/auth.ts +26 -3
  120. package/src/cli/commands/image.ts +282 -0
  121. package/src/cli/commands/index.ts +27 -1
  122. package/src/cli/commands/login.ts +334 -0
  123. package/src/cli/commands/storage.ts +733 -0
  124. package/src/cli/commands/templates.ts +14 -7
  125. package/src/cli/deviceLogin.ts +226 -0
  126. package/src/cli/helpers.ts +204 -27
  127. package/src/cli/secretInput.ts +12 -0
  128. package/src/cli/storageCatalog.ts +192 -0
  129. package/src/cli/storagePublic.ts +15 -0
  130. package/src/cli/storageReceipts.ts +307 -0
  131. package/src/cli/storageSnippets.ts +96 -0
  132. package/src/storageImage.ts +222 -0
  133. package/src/storagePublicPrefixes.ts +39 -0
  134. 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
+ }