transloadit 4.7.4 → 4.7.6

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 (142) hide show
  1. package/README.md +888 -5
  2. package/dist/Transloadit.d.ts +3 -3
  3. package/dist/Transloadit.d.ts.map +1 -1
  4. package/dist/Transloadit.js +2 -2
  5. package/dist/Transloadit.js.map +1 -1
  6. package/dist/alphalib/types/assembliesGet.d.ts +5 -0
  7. package/dist/alphalib/types/assembliesGet.d.ts.map +1 -1
  8. package/dist/alphalib/types/assemblyReplay.d.ts +5 -0
  9. package/dist/alphalib/types/assemblyReplay.d.ts.map +1 -1
  10. package/dist/alphalib/types/assemblyReplayNotification.d.ts +5 -0
  11. package/dist/alphalib/types/assemblyReplayNotification.d.ts.map +1 -1
  12. package/dist/alphalib/types/assemblyStatus.d.ts +25 -25
  13. package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
  14. package/dist/alphalib/types/assemblyStatus.js +4 -1
  15. package/dist/alphalib/types/assemblyStatus.js.map +1 -1
  16. package/dist/alphalib/types/bill.d.ts +5 -0
  17. package/dist/alphalib/types/bill.d.ts.map +1 -1
  18. package/dist/alphalib/types/builtinTemplates.d.ts +83 -0
  19. package/dist/alphalib/types/builtinTemplates.d.ts.map +1 -0
  20. package/dist/alphalib/types/builtinTemplates.js +19 -0
  21. package/dist/alphalib/types/builtinTemplates.js.map +1 -0
  22. package/dist/alphalib/types/robots/ai-chat.d.ts.map +1 -1
  23. package/dist/alphalib/types/robots/ai-chat.js +1 -0
  24. package/dist/alphalib/types/robots/ai-chat.js.map +1 -1
  25. package/dist/alphalib/types/skillFrontmatter.d.ts +29 -0
  26. package/dist/alphalib/types/skillFrontmatter.d.ts.map +1 -0
  27. package/dist/alphalib/types/skillFrontmatter.js +19 -0
  28. package/dist/alphalib/types/skillFrontmatter.js.map +1 -0
  29. package/dist/alphalib/types/template.d.ts +36 -0
  30. package/dist/alphalib/types/template.d.ts.map +1 -1
  31. package/dist/alphalib/types/template.js +10 -0
  32. package/dist/alphalib/types/template.js.map +1 -1
  33. package/dist/alphalib/types/templateCredential.d.ts +10 -0
  34. package/dist/alphalib/types/templateCredential.d.ts.map +1 -1
  35. package/dist/cli/commands/assemblies.d.ts +8 -2
  36. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  37. package/dist/cli/commands/assemblies.js +566 -411
  38. package/dist/cli/commands/assemblies.js.map +1 -1
  39. package/dist/cli/commands/index.d.ts.map +1 -1
  40. package/dist/cli/commands/index.js +5 -0
  41. package/dist/cli/commands/index.js.map +1 -1
  42. package/dist/cli/commands/templates.d.ts.map +1 -1
  43. package/dist/cli/commands/templates.js +4 -14
  44. package/dist/cli/commands/templates.js.map +1 -1
  45. package/dist/cli/fileProcessingOptions.d.ts +35 -0
  46. package/dist/cli/fileProcessingOptions.d.ts.map +1 -0
  47. package/dist/cli/fileProcessingOptions.js +182 -0
  48. package/dist/cli/fileProcessingOptions.js.map +1 -0
  49. package/dist/cli/generateIntentDocs.d.ts +2 -0
  50. package/dist/cli/generateIntentDocs.d.ts.map +1 -0
  51. package/dist/cli/generateIntentDocs.js +321 -0
  52. package/dist/cli/generateIntentDocs.js.map +1 -0
  53. package/dist/cli/intentCommandSpecs.d.ts +36 -0
  54. package/dist/cli/intentCommandSpecs.d.ts.map +1 -0
  55. package/dist/cli/intentCommandSpecs.js +181 -0
  56. package/dist/cli/intentCommandSpecs.js.map +1 -0
  57. package/dist/cli/intentCommands.d.ts +13 -0
  58. package/dist/cli/intentCommands.d.ts.map +1 -0
  59. package/dist/cli/intentCommands.js +368 -0
  60. package/dist/cli/intentCommands.js.map +1 -0
  61. package/dist/cli/intentFields.d.ts +25 -0
  62. package/dist/cli/intentFields.d.ts.map +1 -0
  63. package/dist/cli/intentFields.js +298 -0
  64. package/dist/cli/intentFields.js.map +1 -0
  65. package/dist/cli/intentInputPolicy.d.ts +10 -0
  66. package/dist/cli/intentInputPolicy.d.ts.map +1 -0
  67. package/dist/cli/intentInputPolicy.js +2 -0
  68. package/dist/cli/intentInputPolicy.js.map +1 -0
  69. package/dist/cli/intentRuntime.d.ts +114 -0
  70. package/dist/cli/intentRuntime.d.ts.map +1 -0
  71. package/dist/cli/intentRuntime.js +464 -0
  72. package/dist/cli/intentRuntime.js.map +1 -0
  73. package/dist/cli/resultFiles.d.ts +19 -0
  74. package/dist/cli/resultFiles.d.ts.map +1 -0
  75. package/dist/cli/resultFiles.js +66 -0
  76. package/dist/cli/resultFiles.js.map +1 -0
  77. package/dist/cli/resultUrls.d.ts +19 -0
  78. package/dist/cli/resultUrls.d.ts.map +1 -0
  79. package/dist/cli/resultUrls.js +36 -0
  80. package/dist/cli/resultUrls.js.map +1 -0
  81. package/dist/cli/semanticIntents/imageDescribe.d.ts +43 -0
  82. package/dist/cli/semanticIntents/imageDescribe.d.ts.map +1 -0
  83. package/dist/cli/semanticIntents/imageDescribe.js +188 -0
  84. package/dist/cli/semanticIntents/imageDescribe.js.map +1 -0
  85. package/dist/cli/semanticIntents/index.d.ts +18 -0
  86. package/dist/cli/semanticIntents/index.d.ts.map +1 -0
  87. package/dist/cli/semanticIntents/index.js +18 -0
  88. package/dist/cli/semanticIntents/index.js.map +1 -0
  89. package/dist/cli/semanticIntents/markdownPdf.d.ts +4 -0
  90. package/dist/cli/semanticIntents/markdownPdf.d.ts.map +1 -0
  91. package/dist/cli/semanticIntents/markdownPdf.js +93 -0
  92. package/dist/cli/semanticIntents/markdownPdf.js.map +1 -0
  93. package/dist/cli/semanticIntents/parsing.d.ts +11 -0
  94. package/dist/cli/semanticIntents/parsing.d.ts.map +1 -0
  95. package/dist/cli/semanticIntents/parsing.js +29 -0
  96. package/dist/cli/semanticIntents/parsing.js.map +1 -0
  97. package/dist/cli/stepsInput.d.ts +4 -0
  98. package/dist/cli/stepsInput.d.ts.map +1 -0
  99. package/dist/cli/stepsInput.js +23 -0
  100. package/dist/cli/stepsInput.js.map +1 -0
  101. package/dist/cli.d.ts +1 -1
  102. package/dist/cli.d.ts.map +1 -1
  103. package/dist/cli.js +5 -4
  104. package/dist/cli.js.map +1 -1
  105. package/dist/ensureUniqueCounter.d.ts +8 -0
  106. package/dist/ensureUniqueCounter.d.ts.map +1 -0
  107. package/dist/ensureUniqueCounter.js +48 -0
  108. package/dist/ensureUniqueCounter.js.map +1 -0
  109. package/dist/inputFiles.d.ts +9 -0
  110. package/dist/inputFiles.d.ts.map +1 -1
  111. package/dist/inputFiles.js +177 -26
  112. package/dist/inputFiles.js.map +1 -1
  113. package/dist/robots.js +1 -1
  114. package/dist/robots.js.map +1 -1
  115. package/package.json +9 -7
  116. package/src/Transloadit.ts +3 -3
  117. package/src/alphalib/types/assemblyStatus.ts +4 -1
  118. package/src/alphalib/types/builtinTemplates.ts +24 -0
  119. package/src/alphalib/types/robots/ai-chat.ts +1 -0
  120. package/src/alphalib/types/skillFrontmatter.ts +24 -0
  121. package/src/alphalib/types/template.ts +14 -0
  122. package/src/cli/commands/assemblies.ts +825 -505
  123. package/src/cli/commands/index.ts +6 -3
  124. package/src/cli/commands/templates.ts +6 -17
  125. package/src/cli/fileProcessingOptions.ts +294 -0
  126. package/src/cli/generateIntentDocs.ts +419 -0
  127. package/src/cli/intentCommandSpecs.ts +282 -0
  128. package/src/cli/intentCommands.ts +525 -0
  129. package/src/cli/intentFields.ts +403 -0
  130. package/src/cli/intentInputPolicy.ts +11 -0
  131. package/src/cli/intentRuntime.ts +734 -0
  132. package/src/cli/resultFiles.ts +105 -0
  133. package/src/cli/resultUrls.ts +72 -0
  134. package/src/cli/semanticIntents/imageDescribe.ts +254 -0
  135. package/src/cli/semanticIntents/index.ts +48 -0
  136. package/src/cli/semanticIntents/markdownPdf.ts +120 -0
  137. package/src/cli/semanticIntents/parsing.ts +56 -0
  138. package/src/cli/stepsInput.ts +32 -0
  139. package/src/cli.ts +5 -4
  140. package/src/ensureUniqueCounter.ts +75 -0
  141. package/src/inputFiles.ts +277 -26
  142. package/src/robots.ts +1 -1
package/src/cli.ts CHANGED
@@ -6,6 +6,7 @@ import process from 'node:process'
6
6
  import { fileURLToPath } from 'node:url'
7
7
  import 'dotenv/config'
8
8
  import { createCli } from './cli/commands/index.ts'
9
+ import { ensureError } from './cli/types.ts'
9
10
 
10
11
  const currentFile = realpathSync(fileURLToPath(import.meta.url))
11
12
 
@@ -32,13 +33,13 @@ export async function main(args = process.argv.slice(2)): Promise<void> {
32
33
  }
33
34
  }
34
35
 
35
- export function runCliWhenExecuted(): void {
36
+ export async function runCliWhenExecuted(): Promise<void> {
36
37
  if (!shouldRunCli(process.argv[1])) return
37
38
 
38
- void main().catch((error) => {
39
- console.error((error as Error).message)
39
+ await main().catch((error) => {
40
+ console.error(ensureError(error).message)
40
41
  process.exitCode = 1
41
42
  })
42
43
  }
43
44
 
44
- runCliWhenExecuted()
45
+ await runCliWhenExecuted()
@@ -0,0 +1,75 @@
1
+ const uniqueCounterScopes = new WeakMap<object, Promise<void>>()
2
+
3
+ async function runEnsureUniqueCounterValue<T>({
4
+ initialValue,
5
+ isTaken,
6
+ reserve,
7
+ nextValue,
8
+ }: {
9
+ initialValue: T
10
+ isTaken: (candidate: T) => Promise<boolean> | boolean
11
+ reserve: (candidate: T) => void
12
+ nextValue: (counter: number) => T
13
+ }): Promise<T> {
14
+ let candidate = initialValue
15
+ let counter = 1
16
+
17
+ while (await isTaken(candidate)) {
18
+ candidate = nextValue(counter)
19
+ counter += 1
20
+ }
21
+
22
+ reserve(candidate)
23
+ return candidate
24
+ }
25
+
26
+ export async function ensureUniqueCounterValue<T>({
27
+ initialValue,
28
+ isTaken,
29
+ reserve,
30
+ nextValue,
31
+ scope,
32
+ }: {
33
+ initialValue: T
34
+ isTaken: (candidate: T) => Promise<boolean> | boolean
35
+ reserve: (candidate: T) => void
36
+ nextValue: (counter: number) => T
37
+ scope?: object
38
+ }): Promise<T> {
39
+ if (scope == null) {
40
+ return await runEnsureUniqueCounterValue({
41
+ initialValue,
42
+ isTaken,
43
+ reserve,
44
+ nextValue,
45
+ })
46
+ }
47
+
48
+ const previous = uniqueCounterScopes.get(scope) ?? Promise.resolve()
49
+ let releaseScope: (() => void) | undefined
50
+ const pendingScope = new Promise<void>((resolve) => {
51
+ releaseScope = resolve
52
+ })
53
+ const currentScope = previous
54
+ .catch(() => undefined)
55
+ .then(async () => {
56
+ await pendingScope
57
+ })
58
+ uniqueCounterScopes.set(scope, currentScope)
59
+
60
+ await previous.catch(() => undefined)
61
+
62
+ try {
63
+ return await runEnsureUniqueCounterValue({
64
+ initialValue,
65
+ isTaken,
66
+ reserve,
67
+ nextValue,
68
+ })
69
+ } finally {
70
+ releaseScope?.()
71
+ if (uniqueCounterScopes.get(scope) === currentScope) {
72
+ uniqueCounterScopes.delete(scope)
73
+ }
74
+ }
75
+ }
package/src/inputFiles.ts CHANGED
@@ -1,13 +1,17 @@
1
+ import * as dnsPromises from 'node:dns/promises'
1
2
  import { createWriteStream } from 'node:fs'
2
3
  import { mkdtemp, rm, writeFile } from 'node:fs/promises'
3
4
  import { isIP } from 'node:net'
4
5
  import { tmpdir } from 'node:os'
5
- import { basename, join } from 'node:path'
6
+ import { basename, join, parse } from 'node:path'
6
7
  import type { Readable } from 'node:stream'
7
8
  import { pipeline } from 'node:stream/promises'
9
+ import type CacheableLookup from 'cacheable-lookup'
10
+ import type { EntryObject, IPFamily } from 'cacheable-lookup'
8
11
  import got from 'got'
9
12
  import type { Input as IntoStreamInput } from 'into-stream'
10
13
  import type { CreateAssemblyParams } from './apiTypes.ts'
14
+ import { ensureUniqueCounterValue } from './ensureUniqueCounter.ts'
11
15
 
12
16
  export type InputFile =
13
17
  | {
@@ -63,15 +67,28 @@ const ensureUnique = (field: string, used: Set<string>): void => {
63
67
  used.add(field)
64
68
  }
65
69
 
66
- const ensureUniqueStepName = (baseName: string, used: Set<string>): string => {
67
- let name = baseName
68
- let counter = 1
69
- while (used.has(name)) {
70
- name = `${baseName}_${counter}`
71
- counter += 1
72
- }
73
- used.add(name)
74
- return name
70
+ const ensureUniqueStepName = async (baseName: string, used: Set<string>): Promise<string> =>
71
+ await ensureUniqueCounterValue({
72
+ initialValue: baseName,
73
+ isTaken: (candidate) => used.has(candidate),
74
+ reserve: (candidate) => used.add(candidate),
75
+ nextValue: (counter) => `${baseName}_${counter}`,
76
+ scope: used,
77
+ })
78
+
79
+ const ensureUniqueTempFilePath = async (
80
+ root: string,
81
+ filename: string,
82
+ used: Set<string>,
83
+ ): Promise<string> => {
84
+ const parsedFilename = parse(basename(filename))
85
+ return await ensureUniqueCounterValue({
86
+ initialValue: join(root, parsedFilename.base),
87
+ isTaken: (candidate) => used.has(candidate),
88
+ reserve: (candidate) => used.add(candidate),
89
+ nextValue: (counter) => join(root, `${parsedFilename.name}-${counter}${parsedFilename.ext}`),
90
+ scope: used,
91
+ })
75
92
  }
76
93
 
77
94
  const decodeBase64 = (value: string): Buffer => Buffer.from(value, 'base64')
@@ -106,27 +123,72 @@ const findImportStepName = (field: string, steps: Record<string, unknown>): stri
106
123
  return null
107
124
  }
108
125
 
109
- const downloadUrlToFile = async (url: string, filePath: string): Promise<void> => {
110
- await pipeline(got.stream(url), createWriteStream(filePath))
126
+ const MAX_URL_REDIRECTS = 10
127
+
128
+ const isRedirectStatusCode = (statusCode: number): boolean =>
129
+ statusCode === 301 ||
130
+ statusCode === 302 ||
131
+ statusCode === 303 ||
132
+ statusCode === 307 ||
133
+ statusCode === 308
134
+
135
+ const ipv4FromMappedIpv6 = (address: string): string | null => {
136
+ const lowerAddress = address.toLowerCase()
137
+ const mappedPrefix = lowerAddress.startsWith('::ffff:')
138
+ ? '::ffff:'
139
+ : lowerAddress.startsWith('0:0:0:0:0:ffff:')
140
+ ? '0:0:0:0:0:ffff:'
141
+ : null
142
+
143
+ if (mappedPrefix == null) {
144
+ return null
145
+ }
146
+
147
+ const mappedValue = lowerAddress.slice(mappedPrefix.length)
148
+ if (mappedValue.includes('.')) {
149
+ return mappedValue
150
+ }
151
+
152
+ const segments = mappedValue.split(':')
153
+ if (segments.length !== 2) {
154
+ return null
155
+ }
156
+
157
+ const values = segments.map((segment) => Number.parseInt(segment, 16))
158
+ if (values.some((value) => Number.isNaN(value) || value < 0 || value > 0xffff)) {
159
+ return null
160
+ }
161
+
162
+ return values.flatMap((value) => [(value >> 8) & 0xff, value & 0xff]).join('.')
111
163
  }
112
164
 
113
165
  const isPrivateIp = (address: string): boolean => {
114
- if (address === 'localhost') return true
115
- const family = isIP(address)
166
+ const normalizedAddress =
167
+ address.startsWith('[') && address.endsWith(']') ? address.slice(1, -1) : address
168
+ if (normalizedAddress === 'localhost') return true
169
+ const family = isIP(normalizedAddress)
116
170
  if (family === 4) {
117
- const parts = address.split('.').map((chunk) => Number(chunk))
171
+ const parts = normalizedAddress.split('.').map((chunk) => Number(chunk))
118
172
  const [a, b] = parts
119
173
  if (a === 10) return true
120
174
  if (a === 127) return true
121
175
  if (a === 0) return true
176
+ if (a === 100 && b >= 64 && b <= 127) return true
122
177
  if (a === 169 && b === 254) return true
123
178
  if (a === 172 && b >= 16 && b <= 31) return true
179
+ if (a === 192 && b === 0 && parts[2] === 0) return true
124
180
  if (a === 192 && b === 168) return true
181
+ if (a === 198 && (b === 18 || b === 19)) return true
125
182
  return false
126
183
  }
127
184
  if (family === 6) {
128
- const normalized = address.toLowerCase()
129
- if (normalized === '::1') return true
185
+ const normalized = normalizedAddress.toLowerCase().split('%')[0]
186
+ if (normalized === '::1' || normalized === '0:0:0:0:0:0:0:1') return true
187
+ if (normalized === '::' || normalized === '0:0:0:0:0:0:0:0') return true
188
+ const mappedAddress = ipv4FromMappedIpv6(normalized)
189
+ if (mappedAddress != null && isPrivateIp(mappedAddress)) {
190
+ return true
191
+ }
130
192
  if (normalized.startsWith('fe80:')) return true
131
193
  if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true
132
194
  return false
@@ -134,14 +196,201 @@ const isPrivateIp = (address: string): boolean => {
134
196
  return false
135
197
  }
136
198
 
137
- const assertPublicDownloadUrl = (value: string): void => {
199
+ export const resolvePublicDownloadAddresses = async (
200
+ value: string,
201
+ ): Promise<Array<{ address: string; family: 4 | 6 }>> => {
138
202
  const parsed = new URL(value)
203
+ const hostname =
204
+ parsed.hostname.startsWith('[') && parsed.hostname.endsWith(']')
205
+ ? parsed.hostname.slice(1, -1)
206
+ : parsed.hostname
139
207
  if (!['http:', 'https:'].includes(parsed.protocol)) {
140
208
  throw new Error(`URL downloads are limited to http/https: ${value}`)
141
209
  }
142
- if (isPrivateIp(parsed.hostname)) {
210
+ if (isPrivateIp(hostname)) {
143
211
  throw new Error(`URL downloads are limited to public hosts: ${value}`)
144
212
  }
213
+
214
+ const literalFamily = isIP(hostname)
215
+ const resolvedAddresses =
216
+ literalFamily !== 0
217
+ ? [{ address: hostname, family: literalFamily as 4 | 6 }]
218
+ : await dnsPromises.lookup(hostname, {
219
+ all: true,
220
+ verbatim: true,
221
+ })
222
+ if (resolvedAddresses.some((address) => isPrivateIp(address.address))) {
223
+ throw new Error(`URL downloads are limited to public hosts: ${value}`)
224
+ }
225
+
226
+ if (resolvedAddresses.length === 0) {
227
+ throw new Error(`Unable to resolve URL hostname: ${value}`)
228
+ }
229
+
230
+ return resolvedAddresses.map((address) => ({
231
+ address: address.address,
232
+ family: address.family as 4 | 6,
233
+ }))
234
+ }
235
+
236
+ export function createPinnedDnsLookup(
237
+ validatedAddresses: Array<{ address: string; family: 4 | 6 }>,
238
+ ): CacheableLookup['lookup'] {
239
+ const pinnedAddresses = [...validatedAddresses]
240
+
241
+ function pickAddress(family?: IPFamily): { address: string; family: 4 | 6 } | null {
242
+ if (family == null) {
243
+ return pinnedAddresses[0] ?? null
244
+ }
245
+
246
+ return pinnedAddresses.find((address) => address.family === family) ?? null
247
+ }
248
+
249
+ function pinnedDnsLookup(
250
+ _hostname: string,
251
+ family: IPFamily,
252
+ callback: (error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void,
253
+ ): void
254
+ function pinnedDnsLookup(
255
+ _hostname: string,
256
+ callback: (error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void,
257
+ ): void
258
+ function pinnedDnsLookup(
259
+ _hostname: string,
260
+ options: { all: true },
261
+ callback: (error: NodeJS.ErrnoException | null, result: ReadonlyArray<EntryObject>) => void,
262
+ ): void
263
+ function pinnedDnsLookup(
264
+ _hostname: string,
265
+ options: object,
266
+ callback: (error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void,
267
+ ): void
268
+ function pinnedDnsLookup(
269
+ _hostname: string,
270
+ familyOrCallback:
271
+ | IPFamily
272
+ | object
273
+ | ((error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void),
274
+ callback?:
275
+ | ((error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void)
276
+ | ((error: NodeJS.ErrnoException | null, result: ReadonlyArray<EntryObject>) => void),
277
+ ): void {
278
+ if (typeof familyOrCallback === 'function') {
279
+ const address = pickAddress()
280
+ if (address == null) {
281
+ familyOrCallback(
282
+ new Error('No validated addresses available') as NodeJS.ErrnoException,
283
+ '',
284
+ 4,
285
+ )
286
+ return
287
+ }
288
+ familyOrCallback(null, address.address, address.family)
289
+ return
290
+ }
291
+
292
+ if (
293
+ typeof familyOrCallback === 'object' &&
294
+ familyOrCallback != null &&
295
+ 'all' in familyOrCallback
296
+ ) {
297
+ ;(
298
+ callback as (
299
+ error: NodeJS.ErrnoException | null,
300
+ result: ReadonlyArray<EntryObject>,
301
+ ) => void
302
+ )(
303
+ null,
304
+ pinnedAddresses.map((address) => ({
305
+ address: address.address,
306
+ family: address.family,
307
+ expires: 0,
308
+ })),
309
+ )
310
+ return
311
+ }
312
+
313
+ const family = typeof familyOrCallback === 'number' ? familyOrCallback : undefined
314
+ const address = pickAddress(family)
315
+ if (address == null) {
316
+ ;(
317
+ callback as (error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void
318
+ )(new Error('No validated addresses available') as NodeJS.ErrnoException, '', family ?? 4)
319
+ return
320
+ }
321
+
322
+ ;(callback as (error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void)(
323
+ null,
324
+ address.address,
325
+ address.family,
326
+ )
327
+ }
328
+
329
+ return pinnedDnsLookup
330
+ }
331
+
332
+ const downloadUrlToFile = async ({
333
+ allowPrivateUrls,
334
+ filePath,
335
+ url,
336
+ }: {
337
+ allowPrivateUrls: boolean
338
+ filePath: string
339
+ url: string
340
+ }): Promise<void> => {
341
+ let currentUrl = url
342
+
343
+ for (let redirectCount = 0; redirectCount <= MAX_URL_REDIRECTS; redirectCount += 1) {
344
+ let validatedAddresses: Array<{ address: string; family: 4 | 6 }> | null = null
345
+ if (!allowPrivateUrls) {
346
+ validatedAddresses = await resolvePublicDownloadAddresses(currentUrl)
347
+ }
348
+
349
+ const dnsLookup: CacheableLookup['lookup'] | undefined =
350
+ validatedAddresses == null ? undefined : createPinnedDnsLookup(validatedAddresses)
351
+
352
+ const responseStream = got.stream(currentUrl, {
353
+ dnsLookup,
354
+ followRedirect: false,
355
+ retry: { limit: 0 },
356
+ throwHttpErrors: false,
357
+ })
358
+
359
+ const response = await new Promise<
360
+ Readable & { headers: Record<string, string>; statusCode?: number }
361
+ >((resolvePromise, reject) => {
362
+ responseStream.once('response', (incomingResponse) => {
363
+ resolvePromise(
364
+ incomingResponse as Readable & {
365
+ headers: Record<string, string>
366
+ statusCode?: number
367
+ },
368
+ )
369
+ })
370
+ responseStream.once('error', reject)
371
+ })
372
+
373
+ const statusCode = response.statusCode ?? 0
374
+ if (isRedirectStatusCode(statusCode)) {
375
+ responseStream.destroy()
376
+ const location = response.headers.location
377
+ if (location == null) {
378
+ throw new Error(`Redirect response missing Location header: ${currentUrl}`)
379
+ }
380
+ currentUrl = new URL(location, currentUrl).toString()
381
+ continue
382
+ }
383
+
384
+ if (statusCode >= 400) {
385
+ responseStream.destroy()
386
+ throw new Error(`Failed to download URL: ${currentUrl} (${statusCode})`)
387
+ }
388
+
389
+ await pipeline(responseStream, createWriteStream(filePath))
390
+ return
391
+ }
392
+
393
+ throw new Error(`Too many redirects while downloading URL input: ${url}`)
145
394
  }
146
395
 
147
396
  export const prepareInputFiles = async (
@@ -176,6 +425,7 @@ export const prepareInputFiles = async (
176
425
  const steps = isRecord(nextParams.steps) ? { ...nextParams.steps } : {}
177
426
  const usedSteps = new Set(Object.keys(steps))
178
427
  const usedFields = new Set<string>()
428
+ const usedTempPaths = new Set<string>()
179
429
  const importUrlsByStep = new Map<string, string[]>()
180
430
  const importStepNames = Object.keys(steps).filter((name) => isHttpImportStep(steps[name]))
181
431
  const sharedImportStep = importStepNames.length === 1 ? importStepNames[0] : null
@@ -211,7 +461,7 @@ export const prepareInputFiles = async (
211
461
  if (base64Strategy === 'tempfile') {
212
462
  const root = await ensureTempRoot()
213
463
  const filename = file.filename ? basename(file.filename) : `${file.field}.bin`
214
- const filePath = join(root, filename)
464
+ const filePath = await ensureUniqueTempFilePath(root, filename, usedTempPaths)
215
465
  await writeFile(filePath, buffer)
216
466
  files[file.field] = filePath
217
467
  } else {
@@ -226,7 +476,7 @@ export const prepareInputFiles = async (
226
476
  urlStrategy === 'import' || (urlStrategy === 'import-if-present' && targetStep)
227
477
 
228
478
  if (shouldImport) {
229
- const stepName = targetStep ?? ensureUniqueStepName(file.field, usedSteps)
479
+ const stepName = targetStep ?? (await ensureUniqueStepName(file.field, usedSteps))
230
480
  const urls = importUrlsByStep.get(stepName) ?? []
231
481
  urls.push(file.url)
232
482
  importUrlsByStep.set(stepName, urls)
@@ -238,11 +488,12 @@ export const prepareInputFiles = async (
238
488
  (file.filename ? basename(file.filename) : null) ??
239
489
  getFilenameFromUrl(file.url) ??
240
490
  `${file.field}.bin`
241
- const filePath = join(root, filename)
242
- if (!allowPrivateUrls) {
243
- assertPublicDownloadUrl(file.url)
244
- }
245
- await downloadUrlToFile(file.url, filePath)
491
+ const filePath = await ensureUniqueTempFilePath(root, filename, usedTempPaths)
492
+ await downloadUrlToFile({
493
+ allowPrivateUrls,
494
+ filePath,
495
+ url: file.url,
496
+ })
246
497
  files[file.field] = filePath
247
498
  }
248
499
  }
package/src/robots.ts CHANGED
@@ -274,7 +274,7 @@ export const listRobots = (options: RobotListOptions = {}): RobotListResult => {
274
274
 
275
275
  const start = options.cursor ? Number.parseInt(options.cursor, 10) : 0
276
276
  const safeStart = Number.isFinite(start) && start > 0 ? start : 0
277
- const safeLimit = options.limit && options.limit > 0 ? options.limit : 20
277
+ const safeLimit = options.limit && options.limit > 0 ? options.limit : 200
278
278
  const page = filtered.slice(safeStart, safeStart + safeLimit)
279
279
  const nextCursor =
280
280
  safeStart + safeLimit < filtered.length ? String(safeStart + safeLimit) : undefined