@transloadit/node 4.2.0 → 4.3.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 (86) hide show
  1. package/README.md +116 -4
  2. package/dist/Transloadit.d.ts +45 -4
  3. package/dist/Transloadit.d.ts.map +1 -1
  4. package/dist/Transloadit.js +104 -21
  5. package/dist/Transloadit.js.map +1 -1
  6. package/dist/alphalib/assembly-linter.d.ts +123 -0
  7. package/dist/alphalib/assembly-linter.d.ts.map +1 -0
  8. package/dist/alphalib/assembly-linter.js +1142 -0
  9. package/dist/alphalib/assembly-linter.js.map +1 -0
  10. package/dist/alphalib/assembly-linter.lang.en.d.ts +87 -0
  11. package/dist/alphalib/assembly-linter.lang.en.d.ts.map +1 -0
  12. package/dist/alphalib/assembly-linter.lang.en.js +326 -0
  13. package/dist/alphalib/assembly-linter.lang.en.js.map +1 -0
  14. package/dist/alphalib/goldenTemplates.d.ts +52 -0
  15. package/dist/alphalib/goldenTemplates.d.ts.map +1 -0
  16. package/dist/alphalib/goldenTemplates.js +46 -0
  17. package/dist/alphalib/goldenTemplates.js.map +1 -0
  18. package/dist/alphalib/object.d.ts +20 -0
  19. package/dist/alphalib/object.d.ts.map +1 -0
  20. package/dist/alphalib/object.js +23 -0
  21. package/dist/alphalib/object.js.map +1 -0
  22. package/dist/alphalib/stepParsing.d.ts +93 -0
  23. package/dist/alphalib/stepParsing.d.ts.map +1 -0
  24. package/dist/alphalib/stepParsing.js +1154 -0
  25. package/dist/alphalib/stepParsing.js.map +1 -0
  26. package/dist/alphalib/templateMerge.d.ts +4 -0
  27. package/dist/alphalib/templateMerge.d.ts.map +1 -0
  28. package/dist/alphalib/templateMerge.js +22 -0
  29. package/dist/alphalib/templateMerge.js.map +1 -0
  30. package/dist/cli/commands/assemblies.d.ts +20 -1
  31. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  32. package/dist/cli/commands/assemblies.js +137 -2
  33. package/dist/cli/commands/assemblies.js.map +1 -1
  34. package/dist/cli/commands/auth.d.ts.map +1 -1
  35. package/dist/cli/commands/auth.js +19 -19
  36. package/dist/cli/commands/auth.js.map +1 -1
  37. package/dist/cli/commands/index.d.ts.map +1 -1
  38. package/dist/cli/commands/index.js +2 -1
  39. package/dist/cli/commands/index.js.map +1 -1
  40. package/dist/cli/docs/assemblyLintingExamples.d.ts +2 -0
  41. package/dist/cli/docs/assemblyLintingExamples.d.ts.map +1 -0
  42. package/dist/cli/docs/assemblyLintingExamples.js +10 -0
  43. package/dist/cli/docs/assemblyLintingExamples.js.map +1 -0
  44. package/dist/cli/helpers.d.ts +11 -0
  45. package/dist/cli/helpers.d.ts.map +1 -1
  46. package/dist/cli/helpers.js +29 -0
  47. package/dist/cli/helpers.js.map +1 -1
  48. package/dist/inputFiles.d.ts +41 -0
  49. package/dist/inputFiles.d.ts.map +1 -0
  50. package/dist/inputFiles.js +214 -0
  51. package/dist/inputFiles.js.map +1 -0
  52. package/dist/lintAssemblyInput.d.ts +10 -0
  53. package/dist/lintAssemblyInput.d.ts.map +1 -0
  54. package/dist/lintAssemblyInput.js +73 -0
  55. package/dist/lintAssemblyInput.js.map +1 -0
  56. package/dist/lintAssemblyInstructions.d.ts +29 -0
  57. package/dist/lintAssemblyInstructions.d.ts.map +1 -0
  58. package/dist/lintAssemblyInstructions.js +33 -0
  59. package/dist/lintAssemblyInstructions.js.map +1 -0
  60. package/dist/robots.d.ts +38 -0
  61. package/dist/robots.d.ts.map +1 -0
  62. package/dist/robots.js +230 -0
  63. package/dist/robots.js.map +1 -0
  64. package/dist/tus.d.ts +5 -1
  65. package/dist/tus.d.ts.map +1 -1
  66. package/dist/tus.js +80 -6
  67. package/dist/tus.js.map +1 -1
  68. package/package.json +5 -2
  69. package/src/Transloadit.ts +170 -26
  70. package/src/alphalib/assembly-linter.lang.en.ts +393 -0
  71. package/src/alphalib/assembly-linter.ts +1475 -0
  72. package/src/alphalib/goldenTemplates.ts +53 -0
  73. package/src/alphalib/object.ts +27 -0
  74. package/src/alphalib/stepParsing.ts +1465 -0
  75. package/src/alphalib/templateMerge.ts +32 -0
  76. package/src/alphalib/typings/json-to-ast.d.ts +34 -0
  77. package/src/cli/commands/assemblies.ts +161 -2
  78. package/src/cli/commands/auth.ts +19 -22
  79. package/src/cli/commands/index.ts +2 -0
  80. package/src/cli/docs/assemblyLintingExamples.ts +9 -0
  81. package/src/cli/helpers.ts +50 -0
  82. package/src/inputFiles.ts +278 -0
  83. package/src/lintAssemblyInput.ts +89 -0
  84. package/src/lintAssemblyInstructions.ts +72 -0
  85. package/src/robots.ts +317 -0
  86. package/src/tus.ts +91 -5
@@ -0,0 +1,278 @@
1
+ import { createWriteStream } from 'node:fs'
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
3
+ import { isIP } from 'node:net'
4
+ import { tmpdir } from 'node:os'
5
+ import { basename, join } from 'node:path'
6
+ import type { Readable } from 'node:stream'
7
+ import { pipeline } from 'node:stream/promises'
8
+ import got from 'got'
9
+ import type { Input as IntoStreamInput } from 'into-stream'
10
+ import type { CreateAssemblyParams } from './apiTypes.ts'
11
+
12
+ export type InputFile =
13
+ | {
14
+ kind: 'path'
15
+ field: string
16
+ path: string
17
+ }
18
+ | {
19
+ kind: 'base64'
20
+ field: string
21
+ base64: string
22
+ filename: string
23
+ contentType?: string
24
+ }
25
+ | {
26
+ kind: 'url'
27
+ field: string
28
+ url: string
29
+ filename?: string
30
+ contentType?: string
31
+ }
32
+
33
+ export type UploadInput = Readable | IntoStreamInput
34
+
35
+ export type Base64Strategy = 'buffer' | 'tempfile'
36
+ export type UrlStrategy = 'import' | 'download' | 'import-if-present'
37
+
38
+ export type PrepareInputFilesOptions = {
39
+ inputFiles?: InputFile[]
40
+ params?: CreateAssemblyParams
41
+ fields?: Record<string, unknown>
42
+ base64Strategy?: Base64Strategy
43
+ urlStrategy?: UrlStrategy
44
+ maxBase64Bytes?: number
45
+ allowPrivateUrls?: boolean
46
+ tempDir?: string
47
+ }
48
+
49
+ export type PrepareInputFilesResult = {
50
+ params: CreateAssemblyParams
51
+ files: Record<string, string>
52
+ uploads: Record<string, UploadInput>
53
+ cleanup: Array<() => Promise<void>>
54
+ }
55
+
56
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
57
+ typeof value === 'object' && value !== null
58
+
59
+ const ensureUnique = (field: string, used: Set<string>): void => {
60
+ if (used.has(field)) {
61
+ throw new Error(`Duplicate file field: ${field}`)
62
+ }
63
+ used.add(field)
64
+ }
65
+
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
75
+ }
76
+
77
+ const decodeBase64 = (value: string): Buffer => Buffer.from(value, 'base64')
78
+
79
+ const estimateBase64DecodedBytes = (value: string): number => {
80
+ const trimmed = value.trim()
81
+ if (!trimmed) return 0
82
+ let padding = 0
83
+ if (trimmed.endsWith('==')) padding = 2
84
+ else if (trimmed.endsWith('=')) padding = 1
85
+ return Math.floor((trimmed.length * 3) / 4) - padding
86
+ }
87
+
88
+ const getFilenameFromUrl = (value: string): string | null => {
89
+ try {
90
+ const pathname = new URL(value).pathname
91
+ const base = basename(pathname)
92
+ if (base && base !== '/' && base !== '.') return base
93
+ } catch {
94
+ return null
95
+ }
96
+ return null
97
+ }
98
+
99
+ const isHttpImportStep = (value: unknown): value is Record<string, unknown> =>
100
+ isRecord(value) && value.robot === '/http/import'
101
+
102
+ const findImportStepName = (field: string, steps: Record<string, unknown>): string | null => {
103
+ if (isHttpImportStep(steps[field])) return field
104
+ const matches = Object.entries(steps).filter(([, step]) => isHttpImportStep(step))
105
+ if (matches.length === 1) return matches[0]?.[0] ?? null
106
+ return null
107
+ }
108
+
109
+ const downloadUrlToFile = async (url: string, filePath: string): Promise<void> => {
110
+ await pipeline(got.stream(url), createWriteStream(filePath))
111
+ }
112
+
113
+ const isPrivateIp = (address: string): boolean => {
114
+ if (address === 'localhost') return true
115
+ const family = isIP(address)
116
+ if (family === 4) {
117
+ const parts = address.split('.').map((chunk) => Number(chunk))
118
+ const [a, b] = parts
119
+ if (a === 10) return true
120
+ if (a === 127) return true
121
+ if (a === 0) return true
122
+ if (a === 169 && b === 254) return true
123
+ if (a === 172 && b >= 16 && b <= 31) return true
124
+ if (a === 192 && b === 168) return true
125
+ return false
126
+ }
127
+ if (family === 6) {
128
+ const normalized = address.toLowerCase()
129
+ if (normalized === '::1') return true
130
+ if (normalized.startsWith('fe80:')) return true
131
+ if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true
132
+ return false
133
+ }
134
+ return false
135
+ }
136
+
137
+ const assertPublicDownloadUrl = (value: string): void => {
138
+ const parsed = new URL(value)
139
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
140
+ throw new Error(`URL downloads are limited to http/https: ${value}`)
141
+ }
142
+ if (isPrivateIp(parsed.hostname)) {
143
+ throw new Error(`URL downloads are limited to public hosts: ${value}`)
144
+ }
145
+ }
146
+
147
+ export const prepareInputFiles = async (
148
+ options: PrepareInputFilesOptions = {},
149
+ ): Promise<PrepareInputFilesResult> => {
150
+ const {
151
+ inputFiles = [],
152
+ params = {},
153
+ fields,
154
+ base64Strategy = 'buffer',
155
+ urlStrategy = 'import',
156
+ maxBase64Bytes,
157
+ allowPrivateUrls = true,
158
+ tempDir,
159
+ } = options
160
+
161
+ let nextParams: CreateAssemblyParams = { ...params }
162
+ const files: Record<string, string> = {}
163
+ const uploads: Record<string, UploadInput> = {}
164
+ const cleanup: Array<() => Promise<void>> = []
165
+
166
+ if (fields && Object.keys(fields).length > 0) {
167
+ nextParams = {
168
+ ...nextParams,
169
+ fields: {
170
+ ...(isRecord(nextParams.fields) ? nextParams.fields : {}),
171
+ ...fields,
172
+ },
173
+ }
174
+ }
175
+
176
+ const steps = isRecord(nextParams.steps) ? { ...nextParams.steps } : {}
177
+ const usedSteps = new Set(Object.keys(steps))
178
+ const usedFields = new Set<string>()
179
+ const importUrlsByStep = new Map<string, string[]>()
180
+ const importStepNames = Object.keys(steps).filter((name) => isHttpImportStep(steps[name]))
181
+ const sharedImportStep = importStepNames.length === 1 ? importStepNames[0] : null
182
+
183
+ let tempRoot: string | null = null
184
+ const ensureTempRoot = async (): Promise<string> => {
185
+ if (!tempRoot) {
186
+ const root = await mkdtemp(join(tempDir ?? tmpdir(), 'transloadit-input-'))
187
+ tempRoot = root
188
+ cleanup.push(() => rm(root, { recursive: true, force: true }))
189
+ }
190
+ return tempRoot
191
+ }
192
+
193
+ try {
194
+ for (const file of inputFiles) {
195
+ ensureUnique(file.field, usedFields)
196
+ if (file.kind === 'path') {
197
+ files[file.field] = file.path
198
+ continue
199
+ }
200
+ if (file.kind === 'base64') {
201
+ if (maxBase64Bytes) {
202
+ const estimated = estimateBase64DecodedBytes(file.base64)
203
+ if (estimated > maxBase64Bytes) {
204
+ throw new Error(`Base64 payload exceeds ${maxBase64Bytes} bytes.`)
205
+ }
206
+ }
207
+ const buffer = decodeBase64(file.base64)
208
+ if (maxBase64Bytes && buffer.length > maxBase64Bytes) {
209
+ throw new Error(`Base64 payload exceeds ${maxBase64Bytes} bytes.`)
210
+ }
211
+ if (base64Strategy === 'tempfile') {
212
+ const root = await ensureTempRoot()
213
+ const filename = file.filename ? basename(file.filename) : `${file.field}.bin`
214
+ const filePath = join(root, filename)
215
+ await writeFile(filePath, buffer)
216
+ files[file.field] = filePath
217
+ } else {
218
+ uploads[file.field] = buffer
219
+ }
220
+ continue
221
+ }
222
+ if (file.kind === 'url') {
223
+ const matchedStep = findImportStepName(file.field, steps)
224
+ const targetStep = matchedStep ?? sharedImportStep
225
+ const shouldImport =
226
+ urlStrategy === 'import' || (urlStrategy === 'import-if-present' && targetStep)
227
+
228
+ if (shouldImport) {
229
+ const stepName = targetStep ?? ensureUniqueStepName(file.field, usedSteps)
230
+ const urls = importUrlsByStep.get(stepName) ?? []
231
+ urls.push(file.url)
232
+ importUrlsByStep.set(stepName, urls)
233
+ continue
234
+ }
235
+
236
+ const root = await ensureTempRoot()
237
+ const filename =
238
+ (file.filename ? basename(file.filename) : null) ??
239
+ getFilenameFromUrl(file.url) ??
240
+ `${file.field}.bin`
241
+ const filePath = join(root, filename)
242
+ if (!allowPrivateUrls) {
243
+ assertPublicDownloadUrl(file.url)
244
+ }
245
+ await downloadUrlToFile(file.url, filePath)
246
+ files[file.field] = filePath
247
+ }
248
+ }
249
+ } catch (error) {
250
+ await Promise.all(cleanup.map((fn) => fn()))
251
+ throw error
252
+ }
253
+
254
+ if (Object.keys(steps).length > 0 || importUrlsByStep.size > 0) {
255
+ if (importUrlsByStep.size > 0) {
256
+ for (const [stepName, urls] of importUrlsByStep.entries()) {
257
+ const existing = isRecord(steps[stepName]) ? steps[stepName] : {}
258
+ steps[stepName] = {
259
+ ...existing,
260
+ robot: '/http/import',
261
+ url: urls.length === 1 ? urls[0] : urls,
262
+ }
263
+ }
264
+ }
265
+
266
+ nextParams = {
267
+ ...nextParams,
268
+ steps,
269
+ }
270
+ }
271
+
272
+ return {
273
+ params: nextParams,
274
+ files,
275
+ uploads,
276
+ cleanup,
277
+ }
278
+ }
@@ -0,0 +1,89 @@
1
+ import { getIndentation } from './alphalib/stepParsing.ts'
2
+ import { mergeTemplateContent } from './alphalib/templateMerge.ts'
3
+ import type { AssemblyInstructionsInput, StepsInput } from './alphalib/types/template.ts'
4
+ import type { ResponseTemplateContent, TemplateContent } from './apiTypes.ts'
5
+
6
+ const DEFAULT_INDENT = ' '
7
+
8
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
9
+ typeof value === 'object' && value !== null && !Array.isArray(value)
10
+
11
+ export interface BuildLintInputResult {
12
+ lintContent: string
13
+ wasStepsOnly: boolean
14
+ indent: string
15
+ }
16
+
17
+ export const unwrapStepsOnly = (content: string, indent: string): string => {
18
+ try {
19
+ const parsed = JSON.parse(content)
20
+ if (isRecord(parsed) && 'steps' in parsed) {
21
+ return JSON.stringify((parsed as { steps?: unknown }).steps ?? {}, null, indent)
22
+ }
23
+ } catch (_err) {
24
+ return content
25
+ }
26
+ return content
27
+ }
28
+
29
+ export const buildLintInput = (
30
+ assemblyInstructions?: AssemblyInstructionsInput | StepsInput | string,
31
+ template?: TemplateContent | ResponseTemplateContent,
32
+ ): BuildLintInputResult => {
33
+ let inputString: string | undefined
34
+ let parsedInput: unknown | undefined
35
+ let parseFailed = false
36
+ let indent = DEFAULT_INDENT
37
+
38
+ if (typeof assemblyInstructions === 'string') {
39
+ inputString = assemblyInstructions
40
+ indent = getIndentation(assemblyInstructions)
41
+ try {
42
+ parsedInput = JSON.parse(assemblyInstructions)
43
+ } catch (_err) {
44
+ parseFailed = true
45
+ }
46
+ } else if (assemblyInstructions != null) {
47
+ parsedInput = assemblyInstructions
48
+ }
49
+
50
+ let wasStepsOnly = false
51
+ let instructions: AssemblyInstructionsInput | undefined
52
+
53
+ if (parsedInput !== undefined) {
54
+ if (isRecord(parsedInput)) {
55
+ if ('steps' in parsedInput) {
56
+ instructions = parsedInput as AssemblyInstructionsInput
57
+ } else {
58
+ instructions = { steps: parsedInput as StepsInput }
59
+ wasStepsOnly = true
60
+ }
61
+ } else {
62
+ instructions = { steps: parsedInput as StepsInput }
63
+ wasStepsOnly = true
64
+ }
65
+ }
66
+
67
+ const shouldMergeTemplate = template != null && !parseFailed
68
+ if (shouldMergeTemplate) {
69
+ instructions = mergeTemplateContent(template, instructions)
70
+ }
71
+
72
+ let lintContent = ''
73
+ if (instructions != null) {
74
+ if (
75
+ typeof assemblyInstructions === 'string' &&
76
+ !wasStepsOnly &&
77
+ !parseFailed &&
78
+ !shouldMergeTemplate
79
+ ) {
80
+ lintContent = assemblyInstructions
81
+ } else {
82
+ lintContent = JSON.stringify(instructions, null, indent)
83
+ }
84
+ } else if (inputString != null) {
85
+ lintContent = inputString
86
+ }
87
+
88
+ return { lintContent, wasStepsOnly, indent }
89
+ }
@@ -0,0 +1,72 @@
1
+ import type { HydratedLintIssue } from './alphalib/assembly-linter.lang.en.ts'
2
+ import { hydrateLintIssues } from './alphalib/assembly-linter.lang.en.ts'
3
+ import { applyFix, parseAndLint } from './alphalib/assembly-linter.ts'
4
+ import type { AssemblyInstructionsInput, StepsInput } from './alphalib/types/template.ts'
5
+ import type { ResponseTemplateContent, TemplateContent } from './apiTypes.ts'
6
+ import { buildLintInput, unwrapStepsOnly } from './lintAssemblyInput.ts'
7
+
8
+ export type LintFatalLevel = 'error' | 'warning'
9
+
10
+ export interface LintAssemblyInstructionsInput {
11
+ /**
12
+ * Assembly Instructions as a JSON string, a full instructions object, or a steps-only object.
13
+ */
14
+ assemblyInstructions?: AssemblyInstructionsInput | StepsInput | string
15
+ /**
16
+ * Optional template content to merge with the provided instructions.
17
+ */
18
+ template?: TemplateContent | ResponseTemplateContent
19
+ /**
20
+ * Treat issues at this level or above as fatal. Defaults to "error".
21
+ */
22
+ fatal?: LintFatalLevel
23
+ /**
24
+ * Apply auto-fixes where possible and return the fixed JSON.
25
+ */
26
+ fix?: boolean
27
+ }
28
+
29
+ export interface LintAssemblyInstructionsResult {
30
+ success: boolean
31
+ issues: HydratedLintIssue[]
32
+ fixedInstructions?: string
33
+ }
34
+
35
+ export async function lintAssemblyInstructions(
36
+ options: LintAssemblyInstructionsInput,
37
+ ): Promise<LintAssemblyInstructionsResult> {
38
+ const { assemblyInstructions, template, fix = false, fatal = 'error' } = options
39
+
40
+ if (assemblyInstructions == null && template == null) {
41
+ throw new Error('Provide assemblyInstructions or template content to lint.')
42
+ }
43
+
44
+ const { lintContent, wasStepsOnly, indent } = buildLintInput(assemblyInstructions, template)
45
+
46
+ let issues = await parseAndLint(lintContent)
47
+ let fixedContent = lintContent
48
+
49
+ if (fix) {
50
+ for (const issue of issues) {
51
+ if (!issue.fixId) continue
52
+ // applyFix validates fixData against the fix schema for the fixId.
53
+ fixedContent = applyFix(fixedContent, issue.fixId, issue.fixData as never)
54
+ }
55
+ issues = await parseAndLint(fixedContent)
56
+ }
57
+
58
+ const describedIssues = hydrateLintIssues(issues)
59
+ const fatalTypes = fatal === 'warning' ? new Set(['warning', 'error']) : new Set(['error'])
60
+ const success = !describedIssues.some((issue) => fatalTypes.has(issue.type))
61
+
62
+ const result: LintAssemblyInstructionsResult = {
63
+ success,
64
+ issues: describedIssues,
65
+ }
66
+
67
+ if (fix) {
68
+ result.fixedInstructions = wasStepsOnly ? unwrapStepsOnly(fixedContent, indent) : fixedContent
69
+ }
70
+
71
+ return result
72
+ }